# adapt.auto_cut_down (poll) Source: https://docs.soundpiece.co/api-reference/adapt/auto_cut_down/poll /openapi.json get /v1/adapt.auto_cut_down Fetch the current state of an auto-cut request. Pass `?wait=true` to long-poll for up to 20 seconds, returning early once the operation reaches `ready` or `failed`. # adapt.auto_cut_down (submit) Source: https://docs.soundpiece.co/api-reference/adapt/auto_cut_down/submit /openapi.json put /v1/adapt.auto_cut_down Submit an auto-cut request. Returns immediately with `status: processing`. Use the matching GET to poll for completion, or supply a `callback_url` to receive a webhook. Auto-cut trims a longer audio source to a target duration, choosing musically natural cut points. The output duration may be slightly shorter or longer than `target_duration` to land on a natural boundary. `target_duration` is a target, not an exact length. Its absolute bounds — at least 5 seconds and at most 5 minutes — are checked up front (`422`). It must also be at most twice the source duration; since the source duration is only known after ingestion, exceeding that surfaces asynchronously as a `failed` operation with `target_duration_too_long`. The source must be fetchable, decodable audio within the supported format/size limits and at least 2.5 seconds long (half the minimum target), else a `source_*` error — validated once the source is fetched and decoded (see [errors](/errors)). Add the optional `loudness` block to have the cut normalised to a target integrated loudness, given in LUFS or as a named preset (`streaming`, `podcast` or `broadcast`), at no extra cost — see [loudness](/loudness). **Credits cost:** 1 per request. Insufficient credits returns `402` — see [errors](/errors). # adapt.auto_duck (poll) Source: https://docs.soundpiece.co/api-reference/adapt/auto_duck/poll /openapi.json get /v1/adapt.auto_duck Fetch the current state of an auto-duck request. Pass `?wait=true` to long-poll for up to 20 seconds, returning early once the operation reaches `ready` or `failed`. # adapt.auto_duck (submit) Source: https://docs.soundpiece.co/api-reference/adapt/auto_duck/submit /openapi.json put /v1/adapt.auto_duck Submit an auto-duck request. Returns immediately with `status: processing`. Use the matching GET to poll for completion, or supply a `callback_url` to receive a webhook. Mixes a voice over music, turning the music down while the voice speaks so the speech stays clear. The voice is levelled, the music placed under it, ducked while speech is detected, and the mix delivered at the requested loudness: -14 LUFS by default, with peaks kept at or below -1 dBTP true peak. `loudness` is optional, and `{}` asks for the defaults; it can also name a preset, such as `podcast` (-16 LUFS) — see [loudness](/loudness). The `ready` output reports the loudness measured on the delivered mix. The output is as long as the longer of the two inputs: the shorter one is padded with silence at its end, and nothing is cut. `music` and `voice` must be different sources, else a `422` with `same_source`; two URLs recognisably serving the same file count as the same source. Each must be fetchable, decodable audio within the supported format, length and size limits, else a `source_*` error — validated once it is fetched and decoded, with the error's `source` naming the input that failed when it is known (see [errors](/errors)). **Credits cost:** 1 per request, normalisation included. Insufficient credits returns `402` — see [errors](/errors). # adapt.normalise_loudness (poll) Source: https://docs.soundpiece.co/api-reference/adapt/normalise_loudness/poll /openapi.json get /v1/adapt.normalise_loudness Fetch the current state of a loudness normalisation request. Pass `?wait=true` to long-poll for up to 20 seconds, returning early once the operation reaches `ready` or `failed`. # adapt.normalise_loudness (submit) Source: https://docs.soundpiece.co/api-reference/adapt/normalise_loudness/submit /openapi.json put /v1/adapt.normalise_loudness Submit a loudness normalisation request. Returns immediately with `status: processing`. Use the matching GET to poll for completion, or supply a `callback_url` to receive a webhook. Normalises an audio track to a target integrated loudness (LUFS, as defined by EBU R128), keeping its true peak at or below -1 dBTP. The target defaults to -14 LUFS, the level most streaming services play music back at; `loudness` is optional, and `{}` asks for the defaults. Instead of a number, `loudness` can name a preset for a common destination: `streaming` (-14 LUFS), `podcast` (-16 LUFS) or `broadcast` (-23 LUFS, EBU R128). A preset's value is fixed and never changes. The -1 dBTP ceiling is fixed. Audio whose peaks are loud relative to its average level may not be able to reach the target by a change of level alone without breaking the ceiling. `allow_compression` decides what happens then: compress the dynamics to land close to the target (the default), or keep the dynamics and stop short of it. Either way the `ready` output reports the loudness and true peak measured on the delivered audio, and whether its dynamics were altered. The source must be fetchable, decodable audio within the supported format, length and size limits, else a `source_*` error — validated once the source is fetched and decoded (see [errors](/errors)). **Credits cost:** 1 per request. Insufficient credits returns `402` — see [errors](/errors). # adapt.separate_stems (poll) Source: https://docs.soundpiece.co/api-reference/adapt/separate_stems/poll /openapi.json get /v1/adapt.separate_stems Fetch the current state of a stem separation request. Pass `?wait=true` to long-poll for up to 20 seconds, returning early once the operation reaches `ready` or `failed`. # adapt.separate_stems (submit) Source: https://docs.soundpiece.co/api-reference/adapt/separate_stems/submit /openapi.json put /v1/adapt.separate_stems Submit a stem separation request. Returns immediately with `status: processing`. Use the matching GET to poll for completion, or supply a `callback_url` to receive a webhook. Separates a mixed audio track into its constituent stems (vocals, drums, bass, other, instrumental). The `outputs` list contains one entry per stem with a `kind` discriminator. **Credits cost:** 3 per request (flat, regardless of stem count). Insufficient credits returns `402` — see [errors](/errors). # create.new_fx (poll) Source: https://docs.soundpiece.co/api-reference/create/new_fx/poll /openapi.json get /v1/create.new_fx Fetch the current state of an effect generation request. Pass `?wait=true` to long-poll for up to 20 seconds, returning early once the operation reaches `ready` or `failed`. # create.new_fx (submit) Source: https://docs.soundpiece.co/api-reference/create/new_fx/submit /openapi.json put /v1/create.new_fx Submit an audio effect generation request. Returns immediately with `status: processing`. Use the matching GET to poll for completion, or supply a `callback_url` to receive a webhook. Effects are short, single-shot audio elements like risers, impacts, transitions or foley generated from a text prompt at the requested duration. A prompt rejected by our content policy returns the operation as `failed` with `tos_violation`. **Credits cost:** 3 per request. Insufficient credits returns `402` — see [errors](/errors). # create.new_instrumental (poll) Source: https://docs.soundpiece.co/api-reference/create/new_instrumental/poll /openapi.json get /v1/create.new_instrumental Fetch the current state of an instrumental generation request. Pass `?wait=true` to long-poll for up to 20 seconds, returning early once the operation reaches `ready` or `failed`. # create.new_instrumental (submit) Source: https://docs.soundpiece.co/api-reference/create/new_instrumental/submit /openapi.json put /v1/create.new_instrumental Submit an instrumental generation request. Returns immediately with `status: processing`. Use the matching GET to poll for completion, or supply a `callback_url` to receive a webhook. Instrumentals (no vocals) are generated from either a text prompt or a reference track URL. Set `n_variations` (1-3) for how many variations to produce — each appears as a separate entry in `outputs` when the operation is `ready`. Add the optional `loudness` block to have every variation normalised to a target integrated loudness, given in LUFS or as a named preset (`streaming`, `podcast` or `broadcast`), at no extra cost — see [loudness](/loudness). **Credits cost:** 3 per variation (so `n_variations: 2` costs 6). Insufficient credits returns `402` — see [errors](/errors). # create.new_sample (poll) Source: https://docs.soundpiece.co/api-reference/create/new_sample/poll /openapi.json get /v1/create.new_sample Fetch the current state of a sample generation request. Pass `?wait=true` to long-poll for up to 20 seconds, returning early once the operation reaches `ready` or `failed`. # create.new_sample (submit) Source: https://docs.soundpiece.co/api-reference/create/new_sample/submit /openapi.json put /v1/create.new_sample Submit a sample generation request. Returns immediately with `status: processing`. Use the matching GET to poll for completion, or supply a `callback_url` to receive a webhook. Samples are short audio clips generated from a text prompt at the requested duration. Set `loopable: true` to produce a clip designed to repeat seamlessly. A prompt rejected by our content policy returns the operation as `failed` with `tos_violation`. **Credits cost:** 3 per request. Insufficient credits returns `402` — see [errors](/errors). # create.new_song (poll) Source: https://docs.soundpiece.co/api-reference/create/new_song/poll /openapi.json get /v1/create.new_song Fetch the current state of a song generation request. Pass `?wait=true` to long-poll for up to 20 seconds, returning early once the operation reaches `ready` or `failed`. # create.new_song (submit) Source: https://docs.soundpiece.co/api-reference/create/new_song/submit /openapi.json put /v1/create.new_song Submit a song generation request. Returns immediately with `status: processing`. Use the matching GET to poll for completion, or supply a `callback_url` to receive a webhook. Songs are generated from lyrics plus either a text prompt or a reference track URL. Set `n_variations` (1-3) for how many variations to produce — each appears as a separate entry in `outputs` when the operation is `ready`. Add the optional `loudness` block to have every variation normalised to a target integrated loudness, given in LUFS or as a named preset (`streaming`, `podcast` or `broadcast`), at no extra cost — see [loudness](/loudness). **Credits cost:** 3 per variation (so `n_variations: 2` costs 6). Insufficient credits returns `402` — see [errors](/errors). # create.remix_instrumental (poll) Source: https://docs.soundpiece.co/api-reference/create/remix_instrumental/poll /openapi.json get /v1/create.remix_instrumental Fetch the current state of an instrumental remix request. Pass `?wait=true` to long-poll for up to 20 seconds, returning early once the operation reaches `ready` or `failed`. # create.remix_instrumental (submit) Source: https://docs.soundpiece.co/api-reference/create/remix_instrumental/submit /openapi.json put /v1/create.remix_instrumental Submit an instrumental remix request. Returns immediately with `status: processing`. Use the matching GET to poll for completion, or supply a `callback_url` to receive a webhook. Takes an existing **instrumental** track (no vocals) and produces new instrumental variations of it. By default the source itself guides the output. Optionally supply a `prompt` to steer the result toward a described style or mood — it's echoed back on the operation. For remixing a track that contains vocals, use `create.remix_song` instead. Set `n_variations` (1-3) for how many variations to produce — each appears as a separate entry in `outputs` when the operation is `ready`. Add the optional `loudness` block to have every variation normalised to a target integrated loudness, given in LUFS or as a named preset (`streaming`, `podcast` or `broadcast`), at no extra cost — see [loudness](/loudness). **Credits cost:** 3 per variation (so `n_variations: 3` costs 9). Insufficient credits returns `402` — see [errors](/errors). # create.remix_song (poll) Source: https://docs.soundpiece.co/api-reference/create/remix_song/poll /openapi.json get /v1/create.remix_song Fetch the current state of a song remix request. Pass `?wait=true` to long-poll for up to 20 seconds, returning early once the operation reaches `ready` or `failed`. # create.remix_song (submit) Source: https://docs.soundpiece.co/api-reference/create/remix_song/submit /openapi.json put /v1/create.remix_song Submit a song remix request. Returns immediately with `status: processing`. Use the matching GET to poll for completion, or supply a `callback_url` to receive a webhook. Takes an existing **song with vocals/lyrics** and produces new song variations of it, guided by the `lyrics` and `prompt` you supply. For remixing an instrumental track (no vocals), use `create.remix_instrumental` instead: a source with no recognisable vocals fails the operation with `source_has_no_vocals`. Set `n_variations` (1-3) for how many variations to produce — each appears as a separate entry in `outputs` when the operation is `ready`. Add the optional `loudness` block to have every variation normalised to a target integrated loudness, given in LUFS or as a named preset (`streaming`, `podcast` or `broadcast`), at no extra cost — see [loudness](/loudness). **Credits cost:** 3 per variation (so `n_variations: 2` costs 6). Insufficient credits returns `402` — see [errors](/errors). # Async operations Source: https://docs.soundpiece.co/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_01ks2p3hc0etfb59tdwh9edd1r", status: "processing", … } loop until status is ready or failed Client->>Soundpiece: GET /v1/create.new_song?id=op_01ks2p3hc0etfb59tdwh9edd1r&wait=true Note over Soundpiece: long-polls up to 20s Soundpiece-->>Client: { id: "op_01ks2p3hc0etfb59tdwh9edd1r", status: "processing", … } end Soundpiece-->>Client: { id: "op_01ks2p3hc0etfb59tdwh9edd1r", status: "ready", outputs: [{ url, expires_at, duration }, …] } Client->>Soundpiece: GET 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 Content-Type: application/json { "idempotency_key": "", "lyrics": "I've been driving all night, my hands wet on the wheel", "prompt": "Synthwave, melancholic, mid-tempo" } ``` You get back the operation immediately: ```json theme={null} { "song": { "id": "op_01ks2p3hc0etfb59tdwh9edd1r", "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", "n_variations": 1, "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_01ks2p3hc0etfb59tdwh9edd1r&wait=true Authorization: Bearer ``` If you get back `processing` after 20 seconds, just call again — the request is cheap to repeat. The [reference client](/client)'s `run` function does this loop for you, with a deadline. 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_01ks2p3hc0etfb59tdwh9edd1r", "status": "failed", "error": { "code": "tos_violation", "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 The whole lifecycle, for any endpoint, is three kinds of request. **1. Submit.** Choose an `idempotency_key` and store it with your own record of the job, then send: ```http theme={null} PUT /v1/ Authorization: Bearer Content-Type: application/json { "idempotency_key": "", ...the endpoint's fields... } ``` The `200` response holds one key named for the result (`song`, `auto_cut`, `ducked_audio` and so on), with the operation's `id` and `status: "processing"`. **2. Wait.** Repeat until `status` is no longer `processing`: ```http theme={null} GET /v1/?id=&wait=true Authorization: Bearer ``` Each call returns as soon as the operation finishes, or after 20 seconds. Give up after a deadline of your choosing, or receive a [webhook](/webhooks) instead. **3. Finish.** If `status` is `ready`, download each output's `url` with a plain `GET` (no `Authorization` header) before its `expires_at`. If it is `failed`, read `error.code`. At any step, a network error, a `429` or a `5xx` means: wait (for `Retry-After` seconds, when present) and send the same request again. On a submit, the same `idempotency_key` guarantees the work is only done once. Any other `4xx` means the request needs fixing. The [reference client](/client) is exactly this loop in one small file, and the [Quickstart](/quickstart) shows it in use. For production, receive results by [webhook](/webhooks) and keep polling as the fallback. # Authentication Source: https://docs.soundpiece.co/authentication How to authenticate API requests and manage your API keys ## Overview The Soundpiece API uses **Bearer token authentication**. Every request must include your API key in the `Authorization` header. ```http theme={null} Authorization: Bearer ``` Keys start with the prefix `spk_` — the prefix is part of the key, so use the value exactly as the dashboard shows it. Keys are issued from your account dashboard. The plaintext is shown **once at creation** — save it somewhere safe. Webhook signing secrets (prefixed `whsec_`) used to verify [webhook deliveries](/webhooks) are a **separate resource**, also managed from the dashboard. You can rotate one without touching the other. *** ## Creating a key 1. Open your account dashboard and go to **Developer → API keys**. 2. Click **Create key**, give it a label (e.g. `production-server`), and confirm. 3. The dashboard shows the key value (`spk_…`) once. Copy it into your secrets manager immediately — we don't store the key value and you can't view it again. After you close the dialog, we cannot show you the key again. If you lose it, revoke the key and create a new one. ## Creating a webhook signing secret If you plan to receive webhooks: 1. Go to **Developer → Webhook signing secrets** in the dashboard. 2. Click **Create signing secret**, give it a label, and confirm. 3. The dashboard shows the secret value (`whsec_…`) once. Save it alongside your API key. You can create multiple signing secrets — useful during rotation. See [secret rotation](/webhooks#secret-rotation). *** ## Using your key Pass the key as a Bearer token on every request: ```bash theme={null} curl --request PUT \ --url https://api.soundpiece.co/v1/create.new_song \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ ... }' ``` In application code, load the key from an environment variable or secrets manager — never hardcode it: ```python Python theme={null} import os, requests headers = {"Authorization": f"Bearer {os.environ['SOUNDPIECE_API_KEY']}"} ``` ```javascript Node.js theme={null} const headers = { Authorization: `Bearer ${process.env.SOUNDPIECE_API_KEY}` }; ``` *** ## Key scoping Keys are scoped **per account**. Every key issued to an account can access every endpoint as that account. There is no per-endpoint or per-resource scoping in V1. You can create as many keys as you want — we recommend a separate key per environment (development, staging, production) and per service that calls the API, so you can revoke a single key without disrupting everything. *** ## Rotating a key You can have as many keys active on an account as you want, so rotation is just create-then-delete: 1. Create a new key in the dashboard. 2. Deploy your application with the new key. 3. When the old key has no more traffic against it, delete it. All keys keep working until you delete the old one — there's no cutover window to manage. If you suspect a key has been exposed, rotate immediately. *** ## Secret leak detection Keys are formatted so that automated secret-scanning tools (e.g. GitHub's [secret scanning](https://docs.github.com/en/code-security/secret-scanning)) can recognise them in committed code. If we detect a leak we'll email the account owner and recommend immediate rotation. If you receive such an alert, treat the key as compromised: revoke it from the dashboard and create a new one. *** ## Authentication errors If your API key is missing, invalid, or revoked, the API returns `401 Unauthorized`: ```json theme={null} { "detail": "Missing or invalid Authorization header" } ``` See the [error reference](/errors) for the full list of authentication-related responses. *** ## Enterprise: IP allowlisting Enterprise accounts can restrict API key usage to a specific set of IP addresses or CIDR ranges. Requests from non-allowlisted IPs receive a `401 Unauthorized` response. Contact your account manager to configure allowlisting. # Production checklist Source: https://docs.soundpiece.co/best-practices What every integration should do before it goes live, and why Work through this list before you go live. The [reference client](/client) already does the items marked *(client)*; if you write your own client, it needs to do them too. ## Keys * **Load your API key from the environment or a secrets manager, never from code.** *(client)* Code gets committed, shared and shipped to browsers; anyone who finds the key can spend your credits. See [Authentication](/authentication). ## Submitting work * **Use one idempotency key per job. Store it before the first attempt, and reuse it on every retry and every re-run.** The same key returns the original operation instead of starting the work again, so neither a dropped connection nor a restart of your program creates duplicate work or a duplicate charge. The client reuses its key across retries; to survive a restart, [pass it a key you stored](/client#keeping-an-idempotency-key-across-re-runs). * **Put a timeout on every request.** *(client)* Without one, a stalled connection hangs your program forever. Use more than 20 seconds, so a long poll can finish. * **Retry network errors, `429` and `5xx`, waiting as long as `Retry-After` says (or backing off exponentially when it's absent). Never retry any other `4xx` unchanged.** *(client)* The first group is temporary and safe to repeat with the same idempotency key; any other `4xx` means the request itself is wrong, so sending it again fails again. A `403` means your account doesn't currently have API access; retrying won't change that, so contact support. See [Rate limits](/rate-limits) and [Errors](/errors). * **Handle `402 Payment Required`.** It means your account doesn't have enough credits for the request. Nothing was started or charged, so top up and send the same request again with the same idempotency key. See [Errors](/errors#insufficient-credits-402). ## Waiting for results * **Poll with `wait=true`, and give up after a deadline; or use webhooks.** *(client: polling)* A long poll returns the moment the work finishes, without hammering the API, and a deadline stops one stuck job from blocking your program forever. Webhooks remove the waiting altogether. See [Async operations](/async-operations). ## Webhooks * **Verify the signature of every delivery.** Your webhook URL is public; the signature is what proves a delivery came from Soundpiece and wasn't changed on the way. See [Signature verification](/webhooks#signature-verification). * **Return a `2xx` straight away, and do the work afterwards.** A delivery must be answered within 10 seconds, or it counts as failed and is sent again. Downloading audio inside the handler can easily take longer. * **Deduplicate on the `webhook-id` header.** A delivery can arrive more than once (for example after a timeout), and every attempt carries the same `webhook-id`. * **Fall back to polling.** Polling is the source of truth. If a webhook hasn't arrived when you expected it, poll the operation. ## Results * **Download the audio promptly, and never store the download URL.** Download URLs are signed and expire (see `expires_at`). When you need a fresh one, poll the operation again. Keep the audio you downloaded, not the link. * **Branch on `error.code`, never on `error.message`.** Codes are stable identifiers; messages are written for people and can be reworded at any time. See [Errors](/errors#operation-level-failures). # Build with AI Source: https://docs.soundpiece.co/build-with-ai Give your coding assistant what it needs to write a correct Soundpiece integration AI coding assistants write better integrations when they can read these docs instead of guessing. There are four ways to give them access, from most to least capable. Whichever you use, start your assistant off with the [starter prompt](#starter-prompt) below. ## Install the Soundpiece skill A skill is a set of instructions an agent loads when a task needs it. The Soundpiece skill tells it how the API works, which endpoint does what and what it costs, how to use the [reference client](/client), and what the [production checklist](/best-practices) asks for. Install it with: ```bash theme={null} npx skills add https://docs.soundpiece.co ``` You can also read it directly at [docs.soundpiece.co/skill.md](https://docs.soundpiece.co/skill.md). ## Connect the docs MCP server These docs run an [MCP](https://modelcontextprotocol.io) server, so an assistant can search them and read any page while it works. Its URL is: ```text theme={null} https://docs.soundpiece.co/mcp ``` * **Claude:** in **Settings → Connectors**, choose **Add custom connector** and paste the URL. * **Claude Code:** run `claude mcp add --transport http soundpiece-docs https://docs.soundpiece.co/mcp`. * **Cursor** and **VS Code:** use **Connect to Cursor** or **Connect to VS Code** in the menu at the top of any page. * **Anything else:** add the URL as a remote (HTTP) MCP server in its settings. Agents connected this way can also find the Soundpiece skill through the server. ## Point it at llms.txt Two plain-text files describe the whole site for language models: * [`llms.txt`](https://docs.soundpiece.co/llms.txt) lists every page, with a one-line summary and a link to it. * [`llms-full.txt`](https://docs.soundpiece.co/llms-full.txt) is the full text of every page in one file, to paste in or attach. Every page is also available as Markdown: add `.md` to its URL, for example [docs.soundpiece.co/client.md](https://docs.soundpiece.co/client.md). ## Use the page menu The menu at the top of every page can: * **Copy page** as Markdown, to paste into a chat; * **View as Markdown**; * **Open in ChatGPT**, **Open in Claude** or **Open in Perplexity**, with the page already in the conversation; * **Copy MCP server URL**, or **Connect to Cursor** and **Connect to VS Code**. ## Starter prompt Paste this at the start of a session, then describe what you want to build. ```text theme={null} I'm integrating the Soundpiece audio API. Before writing any code: 1. Read the Soundpiece docs: use the Soundpiece skill or docs MCP server if you have them, otherwise fetch https://docs.soundpiece.co/llms.txt and the pages it links to. 2. Use the reference client from https://docs.soundpiece.co/client.md. Copy the file for my language (soundpiece.py or soundpiece.mjs) into the project unchanged, and call run() and download() rather than writing your own HTTP calls. 3. Follow the production checklist at https://docs.soundpiece.co/best-practices.md. In particular: read the API key from the SOUNDPIECE_API_KEY environment variable; create one idempotency key per job, store it before the first request, and pass it to run() on every attempt; branch on error.code, never error.message; download results promptly instead of storing their URLs. 4. Take request fields, limits and credit costs from the API Reference, not from memory. What I want to build: ``` # Changelog Source: https://docs.soundpiece.co/changelog A record of API changes, new features, and deprecations Entries are dated when a change becomes available, newest first. Subscribe to changelog updates via the [status page](https://status.soundpiece.co.uk) or by emailing [developers@soundpiece.co.uk](mailto:developers@soundpiece.co.uk) to be added to the developer mailing list. *** ## 2026-10-10 — Loudness presets, a reference client and per-endpoint guides **New fields** * `loudness.preset` — on every endpoint that takes a `loudness` block, name a destination instead of a number: `streaming` (-14 LUFS), `podcast` (-16 LUFS) or `broadcast` (-23 LUFS). A preset's value is fixed and never changes. Send `preset` or `target_lufs`, not both; `allow_compression` works with either. The operation's `loudness` echo carries `preset` when you named one (and still carries `target_lufs`, the preset's value). See [loudness](/loudness#presets). **Documentation** * [The reference client](/client): one small file for Python or JavaScript that submits, waits, retries and downloads, with the details that are easy to get wrong already handled. * [Production checklist](/best-practices): what every integration should do before it goes live, and why. * A guide for every endpoint, under **Capabilities**: what it does, a complete example in Python, JavaScript and cURL, the result, its errors and limits, how to choose parameters, and its cost. Start with [Normalise loudness](/guides/adapt/normalise_loudness), which walks through each possible outcome. * API Reference examples: every endpoint's reference page shows example requests and responses, and its code samples include the `Authorization` header. * New API Reference page addresses: `/api-reference///submit` for the `PUT` and `/api-reference///poll` for the `GET`, e.g. `/api-reference/adapt/normalise_loudness/submit`. Old API Reference links redirect to the new pages. *** ## 2026-10-09 — Auto-duck **New endpoints** | Endpoint | Description | | - | - | | `PUT /adapt.auto_duck` | Mix a voice over music, turning the music down while the voice speaks; the mix is delivered normalised, at -14 LUFS by default | **New webhook types** * `adapt.auto_duck.ready` and `adapt.auto_duck.failed`, with the result under `ducked_audio`. **Webhooks** * New `webhook-type` header on every delivery, carrying the kind of event (`.ready` or `.failed`, e.g. `adapt.auto_duck.ready`). The body is the same object the matching `GET` returns, under the same key. See [webhooks](/webhooks#payload-shape). **Errors** * `same_source` — a `422` when `music` and `voice` resolve to the same audio. See [errors](/errors). * `error.source` — on a failed `adapt.auto_duck` operation, names the input (`music` or `voice`) a `source_*` error concerns, when it is known. See [Auto-duck](/guides/adapt/auto_duck) for how the mix is levelled and delivered. *** ## 2026-10-07 — Loudness normalisation **New endpoints** | Endpoint | Description | | - | - | | `PUT /adapt.normalise_loudness` | Normalise a track to a target integrated loudness (-14 LUFS by default), with peaks kept at or below -1 dBTP true peak | **New fields** * `loudness` — an optional block on `create.new_song`, `create.new_instrumental`, `create.remix_song`, `create.remix_instrumental` and `adapt.auto_cut_down` that normalises every output to a target loudness, at no extra cost. Each `ready` output then reports the loudness measured on it (`lufs`, `true_peak_dbtp`, `compressed`). See [loudness](/loudness). *** ## 2026-06-10 — Initial release **New endpoints** | Endpoint | Description | | - | - | | `PUT /create.new_song` | Generate a song with vocals from lyrics plus a prompt or a reference track | | `PUT /create.new_instrumental` | Generate an instrumental from a prompt or a reference track | | `PUT /create.new_sample` | Generate a short sample at a target duration, optionally loopable | | `PUT /create.new_fx` | Generate a one-shot sound effect | | `PUT /create.remix_song` | Remix a song with vocals into new variations, guided by new lyrics and a prompt | | `PUT /create.remix_instrumental` | Remix an instrumental into new variations | | `PUT /adapt.separate_stems` | Separate a track into vocals, drums, bass, other and instrumental stems | | `PUT /adapt.auto_cut_down` | Trim a track to a target duration at musically natural cut points | **Core features** * Bearer API key authentication * An `idempotency_key` on every submit, so retries never create work twice * Asynchronous operations: submit with `PUT`, poll the same endpoint with `GET` (long-polling with `wait=true`) * Webhook callbacks via `callback_url`, signed per the Standard Webhooks spec (`webhook-id`, `webhook-timestamp` and `webhook-signature` headers, HMAC-SHA256) * Results delivered as signed, time-limited FLAC download URLs *** ## Format reference Each entry uses the following change types: | Tag | Meaning | | - | - | | `New` | New endpoint, field, or feature | | `Changed` | Behaviour change to an existing feature (non-breaking) | | `Deprecated` | Feature marked for removal; sunset date given | | `Removed` | Previously deprecated feature has been removed | | `Fixed` | Bug fix | | `Security` | Security-related change | # The reference client Source: https://docs.soundpiece.co/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. # Data & Privacy Source: https://docs.soundpiece.co/data-privacy How Soundpiece handles your audio data, where it is stored, and your rights under UK/EU GDPR This page summarises Soundpiece's data practices in plain language. For additional information, contact [business@soundpiece.co.uk](mailto:business@soundpiece.co.uk). *** ## What data we process When you use the Soundpiece API, we process: | Data type | Purpose | | - | - | | Fetched source audio (from URLs you provide) | Input to adaptation and remix operations | | Lyrics and prompts | Input to generation operations | | Generated audio outputs | Available for you to download | | Operation records | Polling, audit, and support | | API key hashes | Authentication | We store lyrics, prompts, source audio, and generated outputs against your account so you can query past operations, download outputs, and audit your usage. Data is retained until you close your account or request its deletion — see [Audio retention and deletion](#audio-retention-and-deletion). *** ## Data storage and infrastructure All data is stored and processed within the **United Kingdom and European Economic Area (EEA)**. We do not transfer audio data to servers outside the UK/EEA at any point during processing. Our infrastructure is contractually bound by Standard Contractual Clauses (SCCs) and is GDPR-compliant. *** ## Audio retention and deletion We retain your operations and their outputs against your account so you can query past work and re-download outputs over time. Re-fetch a signed URL each time by polling the `GET` endpoint — it regenerates the URL with a fresh expiry. Data is retained until you close your account or request its deletion. To delete a specific operation (for example, when processing a data subject erasure request) contact [business@soundpiece.co.uk](mailto:business@soundpiece.co.uk) with the operation `id`. Deletion is immediate and permanent — the audio content is irrecoverably removed. *** ## Model training Soundpiece does **not** use your uploaded audio, generated outputs, or prompts to train or fine-tune AI models. This is the default for all plans and does not require an opt-out action. Enterprise customers can request written confirmation of this position as part of a DPA. *** ## Third-party subprocessors Soundpiece uses a limited set of subprocessors to operate the service. Key categories: | Category | Purpose | Location | | - | - | - | | Cloud infrastructure | Compute and storage | UK / EEA | | Model inference | Audio generation and adaptation | UK / EEA | | Monitoring and logging | Operational reliability | UK / EEA | | Email (transactional) | Notifications and alerts | EU | Enterprise customers are notified of any changes to the subprocessor list in advance and may raise objections under the terms of their DPA. *** ## Your rights Under UK and EU GDPR, you (and your end users where applicable) have the right to: * **Access** — request a copy of personal data we hold * **Rectification** — correct inaccurate data * **Erasure** — request deletion of personal data * **Restriction** — ask us to stop processing in certain circumstances * **Portability** — receive your data in a machine-readable format * **Object** — object to processing based on legitimate interests To exercise any of these rights, contact [business@soundpiece.co.uk](mailto:business@soundpiece.co.uk). *** ## Questions For any data or privacy questions not covered here, contact [business@soundpiece.co.uk](mailto:business@soundpiece.co.uk). # Errors Source: https://docs.soundpiece.co/errors HTTP error responses and per-operation failure codes ## Why two layers of errors? Soundpiece returns two distinct kinds of error, and they mean different things: * **HTTP-level errors** (`4xx` / `5xx`) — your *request* was rejected. The work never started. * **Operation-level errors** — your request was accepted (you got `200 OK` and an operation `id`), but the *operation itself* failed during processing. You poll the operation just like any other and find `status: "failed"` with a populated `error` object. This split is deliberate: the request layer is about whether you talked to us correctly, the operation layer is about whether the work succeeded. They have different causes, different retry rules, and different recovery strategies. A successful HTTP response doesn't mean the operation succeeded — you still need to check `status`. *** ## HTTP errors The two rules of thumb: * **`4xx` — fix your request.** Something in what you sent doesn't work. Retrying with the same body won't help; you need to change the request. * **`5xx` — something unexpected happened on our side. You can retry.** `PUT` endpoints are idempotent on `idempotency_key` (see [Async operations](/async-operations#idempotency-end-to-end)), so retrying with the same key is safe — you won't double-create work. ### Response shape HTTP errors return a JSON body with a `detail` field. For most errors it's a plain string: ```json theme={null} { "detail": "Missing or invalid Authorization header" } ``` For request-body validation errors (`422`), `detail` is a **list** of per-field entries: ```json theme={null} { "detail": [ { "loc": ["body", "lyrics"], "msg": "String should have at least 1 character", "type": "string_too_short" }, { "loc": ["body", "idempotency_key"], "msg": "Field required", "type": "missing" } ] } ``` | Field | Meaning | | - | - | | `loc` | Path to the offending field — e.g. `["body", "lyrics"]` for a top-level body field, or `["body", "reference", "url"]` for a nested one. | | `msg` | Human-readable description of the problem. | | `type` | Stable machine-readable code (e.g. `missing`, `string_too_short`, `url_parsing`). | ### Status reference | Status | When you'll see it | Recommended handling | | - | - | - | | `400 Bad Request` | Malformed JSON or unparseable request body. | Fix the request. | | `401 Unauthorized` | Missing, invalid, or revoked API key (and, on Enterprise plans with [IP allowlisting](/authentication#enterprise-ip-allowlisting), a request from a non-allowlisted IP). | Check your `Authorization` header and source IP. Don't retry. | | `402 Payment Required` | Your account doesn't have enough credits to perform the requested work. See [Insufficient credits](#insufficient-credits-402) below for the response shape. | Top up your account, then retry with the same `idempotency_key`. | | `403 Forbidden` | Your API key is valid, but your account doesn't currently have API access — for example, the plan that included it has ended. The body is `{"detail": "not_authorized"}`. A correctly set-up account never sees this. | Don't retry. Contact [support](mailto:business@soundpiece.co.uk). | | `404 Not Found` | **Only** returned when the endpoint URL itself doesn't exist — e.g. a typo in the path. Entity-not-found (operation id, etc.) is reported as a `422` validation error, not a `404`. | Check the URL. | | `405 Method Not Allowed` | Wrong HTTP verb on a known route (e.g. `POST` on a `PUT`-only endpoint). | Use the documented verb. | | `410 Gone` | The endpoint existed on this API version but has been sunset. The `detail` field points to the replacement. | Migrate to the replacement endpoint. See [versioning](/versioning). | | `422 Unprocessable Entity` | Request body validation failed — missing field, wrong type, out-of-range value, invalid URL, or a referenced entity (operation `id` etc.) wasn't found. | Read `detail` to see which field is bad. | | `429 Too Many Requests` | You've hit your account's request rate limit. | Read the `Retry-After` header (seconds) and back off. See [rate limits](/rate-limits). | | `5xx` | Transient server-side issue. | **Retry safely** — `PUT` requests are idempotent if you supply the same `idempotency_key`. Use exponential backoff. | ### Insufficient credits (`402`) When you submit a request whose credit cost exceeds your current balance, the call is rejected up-front (no work starts, no charge) with a `402` whose body is: ```json theme={null} { "detail": { "type": "insufficient_credits", "msg": "You have insufficient credits to perform this action." } } ``` The per-endpoint credit cost is documented on each endpoint (e.g. `create.remix_song` and `create.remix_instrumental` charge **3 credits per variation**, so `n_variations: 3` costs 9). Top up your account and re-submit with the same `idempotency_key` — the original request was never persisted, so the retry creates the work fresh. ### Same source (`422`) `adapt.auto_duck` takes two sources, `music` and `voice`, and refuses a request where both resolve to the same audio — the same URL, or two URLs recognisably serving the same file. The request is rejected up-front (no work starts, no charge) with a `422`: ```json theme={null} { "detail": [ { "loc": ["body", "voice", "url"], "msg": "voice must be a different source from music", "type": "same_source" } ] } ``` Send two different sources. ### Source too large (`422`) When a source's server reports the file's size up front and it is over the endpoint's limit, the request is refused straight away (no work starts, no charge) with a `422` of type `source_too_large`, at the source's `url` field (for example `["body", "source", "url"]`). When the size isn't reported, it is checked while processing instead, and the operation fails with the `source_too_large` code below. *** ## Operation-level failures When an operation fails, the response looks like this: ```json theme={null} { "song": { "id": "op_01ks2p3hc0etfb59tdwh9edd1r", "status": "failed", "error": { "code": "tos_violation", "message": "The provided lyrics were rejected by our content policy." }, ... } } ``` `error.code` is a **stable machine-readable identifier** — safe to branch on. `error.message` is a human-readable description suitable for surfacing to your end-users. Not every code applies to every endpoint. The sections below group codes by which endpoints can return them. ### Codes returned by all endpoints | Code | Meaning | Recommended handling | | - | - | - | | `internal_error` | Generic failure with no more specific classification available. | Re-submit with the same `idempotency_key`. If it persists, contact support. | | `generation_timeout` | The operation took longer than our hard timeout to complete. | Re-submit. Persistent timeouts are worth raising with support. | ### Codes for endpoints with a `source` or `reference` (applies to `adapt.separate_stems`, `adapt.auto_cut_down`, `adapt.normalise_loudness`, `adapt.auto_duck`, `create.remix_song`, `create.remix_instrumental`, and the `reference` variant of `create.new_song` / `create.new_instrumental`) | Code | Meaning | | - | - | | `source_unreachable` | We couldn't fetch the URL — DNS failure, connection refused, TLS error, 4xx/5xx from the host. | | `source_format_unsupported` | The file at the URL is not a supported audio format (we accept wav, mp3, flac). | | `source_too_short` | The fetched audio is shorter than this endpoint's limits allow; see the endpoint's page. | | `source_too_long` | The fetched audio is longer than this endpoint's limits allow; see the endpoint's page. | | `source_too_large` | The file at the URL is larger than this endpoint's limits allow; see the endpoint's page. | The accepted length and size differ by endpoint, so they aren't listed here: each endpoint's page gives its own limits (for example [`adapt.auto_cut_down`](/guides/adapt/auto_cut_down#errors) or [`create.remix_song`](/guides/create/remix_song#errors)). ### Codes for `create.remix_song` | Code | Meaning | | - | - | | `source_has_no_vocals` | The source was fetched and is valid audio, but no vocals could be recognised in it. `create.remix_song` keeps the source's vocal line and sets new lyrics to it, so it needs a source with vocals. To remix audio without vocals, use `create.remix_instrumental` instead. | ### Codes for `adapt.auto_cut_down` | Code | Meaning | | - | - | | `target_duration_too_long` | The requested `target_duration` is more than twice the source's duration. The absolute bounds are checked up front (`422`); this source-relative bound is only knowable once the source has been fetched, so it surfaces as a failed operation. | ### Which input failed on `adapt.auto_duck` `adapt.auto_duck` takes two sources, so its `error` carries one more field, `source`, naming the input a `source_*` error (above) concerns — `"music"` or `"voice"`: ```json theme={null} { "ducked_audio": { "id": "op_01ks2pthpgf4qs4fr84g98pbsk", "status": "failed", "error": { "code": "source_unreachable", "message": "The source URL could not be reached.", "source": "voice" }, ... } } ``` `source` is present when the failing input is known. It is absent — not `null` — for every other error, and for a source error that could not be attributed to one input. ### Codes for generation endpoints (applies to `create.new_song`, `create.new_instrumental`, `create.new_sample`, `create.new_fx`, `create.remix_song`, `create.remix_instrumental`) | Code | Meaning | | - | - | | `tos_violation` | The lyrics, prompt, or reference were rejected by our content policy. | *** ## Why retries don't risk double-billing `PUT` endpoints are **idempotent on `idempotency_key`**. The same key always returns the same operation, regardless of how many times you retry. This means: * `5xx` HTTP responses are safely retryable — retry with the same key and you'll either get a fresh attempt or the existing successful result. * `429` is retryable — back off using `Retry-After`, then send the same key again. * `4xx` other than `429` indicate a bad request that won't get better; fix the request before retrying. You will never accidentally create two operations from one logical request, no matter how many times the network drops between you and us. The `idempotency_key` is also **echoed on every response and webhook payload** so you can correlate work against your own records without a separate request-id-to-state map. *** ## Reporting persistent issues If you receive `internal_error` (operation-level) or `5xx` (HTTP-level) that persists across retries, please contact [business@soundpiece.co.uk](mailto:business@soundpiece.co.uk) with: * The endpoint called. * The operation `id` if one was returned. * The `idempotency_key` you sent. * The approximate timestamp. * Any `error.code` / `error.message` from the response body. # Auto cut-down Source: https://docs.soundpiece.co/guides/adapt/auto_cut_down Trim a track to a target length at musically natural cut points `adapt.auto_cut_down` shortens (or lengthens) a piece of music to a length you choose, cutting at points that sound natural instead of fading out mid-phrase. Use it to fit a track to a 30-second advert, a 15-second social clip, or the length of a voiceover. *** ## The request | Field | Required | What it is | | - | - | - | | `source` | yes | The track: `{"url": "https://..."}`, a public link to a WAV, MP3 or FLAC file. | | `target_duration` | yes | The length you want, in seconds, from `5` to `300`, and at most twice the source's length. | | `loudness` | no | Normalise the cut to a target loudness, at no extra cost. Leave it out to get the cut as it is. See [Loudness](/loudness). | | `idempotency_key` | yes | A UUID that identifies this job. The [reference client](/client) fills it in for you. | | `callback_url` | no | An HTTPS address to receive a [webhook](/webhooks) when the work is finished. | Every field, with its exact rules: [submit](/api-reference/adapt/auto_cut_down/submit) and [poll](/api-reference/adapt/auto_cut_down/poll) in the API Reference. *** ## Example This cuts a track down to 30 seconds for an advert, normalised for streaming. It uses the [reference client](/client); save `soundpiece.py` or `soundpiece.mjs` next to it first. ```python cut.py theme={null} import soundpiece cut = soundpiece.run("adapt.auto_cut_down", { "source": {"url": "https://example.com/audio/track.mp3"}, "target_duration": 30, "loudness": {"preset": "streaming"}, }) soundpiece.download(cut["output"]["url"], "cut.flac") print(f"Saved cut.flac ({cut['output']['duration']} seconds)") ``` ```javascript cut.mjs theme={null} import { run, download } from "./soundpiece.mjs"; const cut = await run("adapt.auto_cut_down", { source: { url: "https://example.com/audio/track.mp3" }, target_duration: 30, loudness: { preset: "streaming" }, }); await download(cut.output.url, "cut.flac"); console.log(`Saved cut.flac (${cut.output.duration} seconds)`); ``` ```bash cURL theme={null} #!/usr/bin/env bash # Submit, wait and download by hand. Needs curl, jq and uuidgen. set -euo pipefail API=https://api.soundpiece.co/v1 AUTH="Authorization: Bearer $SOUNDPIECE_API_KEY" # 1. Submit. Sending this exact body again (same idempotency_key) is safe. body=$(jq -n --arg key "$(uuidgen)" '{ idempotency_key: $key, source: { url: "https://example.com/audio/track.mp3" }, target_duration: 30, loudness: { preset: "streaming" } }') response=$(curl --silent --show-error --max-time 30 --request PUT "$API/adapt.auto_cut_down" \ --header "$AUTH" --header "Content-Type: application/json" --data "$body") result=$(jq -e .auto_cut <<<"$response") || { echo "$response"; exit 1; } id=$(jq -r .id <<<"$result") # 2. Wait. Each call returns as soon as the work is finished, or after 20 seconds. while [ "$(jq -r .status <<<"$result")" = processing ]; do response=$(curl --silent --show-error --max-time 30 "$API/adapt.auto_cut_down?id=$id&wait=true" \ --header "$AUTH") result=$(jq -e .auto_cut <<<"$response") || { echo "$response"; exit 1; } done # 3. Download, or show what went wrong. if [ "$(jq -r .status <<<"$result")" = failed ]; then jq .error <<<"$result" exit 1 fi curl --silent --show-error --fail --output cut.flac "$(jq -r .output.url <<<"$result")" echo "Saved cut.flac" ``` *** ## The result `run` returns the finished `auto_cut` object: ```json theme={null} { "id": "op_01ks2q0f8ewz3t6m1n4b7c9d2a", "status": "ready", "created_at": "2026-10-10T09:15:02Z", "idempotency_key": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f", "target_duration": 30.0, "loudness": { "target_lufs": -14.0, "allow_compression": true, "preset": "streaming" }, "output": { "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-10-10T09:31:40Z", "duration": 30.6, "loudness": { "lufs": -14.0, "true_peak_dbtp": -1.4, "compressed": false } }, "error": null } ``` | Field | What it means | | - | - | | `target_duration` | The length you asked for. | | `loudness` | Present only if you asked for normalisation: the target, and `preset` when you named one. | | `output.url` | A signed link to the cut, as a FLAC file (44.1 kHz). It needs no API key. | | `output.expires_at` | When the link stops working. Poll the operation again for a fresh one. | | `output.duration` | The cut's real length in seconds. It can be slightly shorter or longer than `target_duration`, so that it ends on a natural boundary. | | `output.loudness` | Present only if you asked for normalisation: `lufs`, `true_peak_dbtp` and `compressed`, measured on the cut. | *** ## Choosing parameters * **`target_duration` is a target, not an exact length.** The cut lands near it, on a natural boundary. If you need an exact length (to the frame), trim or pad the result yourself afterwards. * **It can lengthen too.** A target longer than the source makes a longer version, up to twice the source's length. * **Loudness** is off unless you include the block. Add it when the cut is going straight to a platform; it costs nothing extra. See the [loudness normalisation guide](/guides/adapt/normalise_loudness#choosing-a-target-by-destination) for which target to pick. *** ## Errors **When you submit** (the request is refused, nothing is charged): | Status | `type` | Why | | - | - | - | | `422` | `greater_than_equal` / `less_than_equal` | `target_duration` is under 5 or over 300 seconds. | | `422` | `source_too_large` | The source's server says the file is over 50 MB. (When it doesn't say, the size is checked during processing instead.) | | `422` | `idempotency_key_reused` | The key was already used for a different endpoint. | | `402` | `insufficient_credits` | Your balance is below 1 credit. | **While processing** (the operation finishes as `failed`, with an `error.code`): | Code | Why | | - | - | | `target_duration_too_long` | `target_duration` is more than twice the source's length. This is only known once the source has been fetched, so it can't be refused up front. | | `source_unreachable` | The source URL couldn't be fetched. | | `source_format_unsupported` | The file isn't WAV, MP3 or FLAC audio. | | `source_too_short` | The audio is shorter than 2.5 seconds. | | `source_too_long` | The audio is longer than 5 minutes. | | `source_too_large` | The file is larger than 50 MB. | | `internal_error`, `generation_timeout` | Something went wrong on our side. Resubmit with the same idempotency key. | **Source limits for this endpoint:** WAV, MP3 or FLAC, from **2.5 seconds** to 5 minutes long, and at most 50 MB. The minimum is higher than elsewhere because the shortest cut, 5 seconds, needs a source at least half that long. See [Errors](/errors) for the response shapes, and for the errors every endpoint can return (`401`, `403`, `429`). *** ## Cost **1 credit** per request, with or without normalisation. Resending a request with the same idempotency key is not charged again. # Auto-duck Source: https://docs.soundpiece.co/guides/adapt/auto_duck Mix a voiceover over music, turning the music down while the voice speaks `adapt.auto_duck` takes a music track and a voice recording and delivers one finished mix, with the music turned down ("ducked") whenever the voice is speaking so every word stays clear. Use it for adverts, explainers, podcast intros, narrated video and anywhere else a voice talks over music. You don't need to balance the two recordings first. In one step: 1. the voice is levelled, so it sits at the same level whether it was recorded quietly or loudly; 2. the music is placed underneath it, at a level that leaves room for the speech; 3. the music is ducked while speech is detected, and comes back up when it stops; 4. the mix is delivered at a target loudness: -14 LUFS unless you ask for another, with its true peak at or below -1 dBTP. *** ## The request | Field | Required | What it is | | - | - | - | | `music` | yes | The music: `{"url": "https://..."}`, a public link to a WAV, MP3 or FLAC file. | | `voice` | yes | The speech, in the same form. It must be a different file from `music`. | | `loudness` | no | The loudness to deliver the mix at, as a preset or a number. Leave it out for -14 LUFS. See [Loudness](/loudness). | | `idempotency_key` | yes | A UUID that identifies this job. The [reference client](/client) fills it in for you. | | `callback_url` | no | An HTTPS address to receive a [webhook](/webhooks) when the work is finished. | Every field, with its exact rules: [submit](/api-reference/adapt/auto_duck/submit) and [poll](/api-reference/adapt/auto_duck/poll) in the API Reference. *** ## Example This mixes a voiceover over a music bed for a podcast intro, delivered with the `podcast` preset (-16 LUFS). It uses the [reference client](/client); save `soundpiece.py` or `soundpiece.mjs` next to it first. ```python duck.py theme={null} import soundpiece mix = soundpiece.run("adapt.auto_duck", { "music": {"url": "https://example.com/audio/music.mp3"}, "voice": {"url": "https://example.com/audio/voiceover.mp3"}, "loudness": {"preset": "podcast"}, }) soundpiece.download(mix["output"]["url"], "mix.flac") print(f"Saved mix.flac ({mix['output']['duration']} seconds)") ``` ```javascript duck.mjs theme={null} import { run, download } from "./soundpiece.mjs"; const mix = await run("adapt.auto_duck", { music: { url: "https://example.com/audio/music.mp3" }, voice: { url: "https://example.com/audio/voiceover.mp3" }, loudness: { preset: "podcast" }, }); await download(mix.output.url, "mix.flac"); console.log(`Saved mix.flac (${mix.output.duration} seconds)`); ``` ```bash cURL theme={null} #!/usr/bin/env bash # Submit, wait and download by hand. Needs curl, jq and uuidgen. set -euo pipefail API=https://api.soundpiece.co/v1 AUTH="Authorization: Bearer $SOUNDPIECE_API_KEY" # 1. Submit. Sending this exact body again (same idempotency_key) is safe. body=$(jq -n --arg key "$(uuidgen)" '{ idempotency_key: $key, music: { url: "https://example.com/audio/music.mp3" }, voice: { url: "https://example.com/audio/voiceover.mp3" }, loudness: { preset: "podcast" } }') response=$(curl --silent --show-error --max-time 30 --request PUT "$API/adapt.auto_duck" \ --header "$AUTH" --header "Content-Type: application/json" --data "$body") result=$(jq -e .ducked_audio <<<"$response") || { echo "$response"; exit 1; } id=$(jq -r .id <<<"$result") # 2. Wait. Each call returns as soon as the work is finished, or after 20 seconds. while [ "$(jq -r .status <<<"$result")" = processing ]; do response=$(curl --silent --show-error --max-time 30 "$API/adapt.auto_duck?id=$id&wait=true" \ --header "$AUTH") result=$(jq -e .ducked_audio <<<"$response") || { echo "$response"; exit 1; } done # 3. Download, or show what went wrong. if [ "$(jq -r .status <<<"$result")" = failed ]; then jq .error <<<"$result" exit 1 fi curl --silent --show-error --fail --output mix.flac "$(jq -r .output.url <<<"$result")" echo "Saved mix.flac" ``` *** ## The result `run` returns the finished `ducked_audio` object: ```json theme={null} { "id": "op_01ks2pthpgf4qs4fr84g98pbsk", "status": "ready", "created_at": "2026-10-10T09:15:02Z", "idempotency_key": "9b2e4c1a-7d3f-4a5b-8c6d-0e1f2a3b4c5d", "loudness": { "target_lufs": -16.0, "allow_compression": true, "preset": "podcast" }, "output": { "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-10-10T09:31:40Z", "duration": 94.2, "loudness": { "lufs": -16.0, "true_peak_dbtp": -1.8, "compressed": false } }, "error": null } ``` | Field | What it means | | - | - | | `loudness` | The loudness the mix was delivered at. `target_lufs` is always filled in; `preset` is there only when you named one. | | `output.url` | A signed link to the mix, as a FLAC file (44.1 kHz). It needs no API key. | | `output.expires_at` | When the link stops working. Poll the operation again for a fresh one. | | `output.duration` | The mix's length in seconds: the length of the **longer** of your two inputs. | | `output.loudness` | Measured on the mix you download: `lufs`, `true_peak_dbtp`, and whether the dynamics were `compressed` to reach the target. | The source URLs are never echoed back. *** ## Choosing parameters * **Length.** The mix is as long as the longer input. The shorter one is padded with silence at its end, so nothing is cut off. To end the music with the voice, trim the music to length first, for example with [`adapt.auto_cut_down`](/guides/adapt/auto_cut_down). * **Loudness.** Pick the preset for where the mix is going: `streaming` (-14 LUFS, the default) for online video and social media, `podcast` (-16 LUFS) for podcast feeds, `broadcast` (-23 LUFS) for EBU R128 television and radio. Or send `{"target_lufs": ...}` for another number between -30 and -6. The [loudness normalisation guide](/guides/adapt/normalise_loudness#choosing-a-target-by-destination) has a fuller table. * **`allow_compression`** works as it does everywhere: on by default, so the mix lands on the target; set it to `false` to keep the dynamics untouched, at the cost of landing a little quieter when the audio is very dynamic. See [Loudness](/loudness#allow_compression). * **Inputs.** A clean voice recording, without its own music under it, gives the best result. The music can be any finished track or bed; you don't need to turn it down beforehand. *** ## Errors **When you submit** (the request is refused, nothing is charged): | Status | `type` | Why | | - | - | - | | `422` | `same_source` | `music` and `voice` are the same file: the same URL, or two URLs recognisably serving the same file. Reported at `["body", "voice", "url"]`. | | `422` | `extra_forbidden` | `loudness` has both `preset` and `target_lufs`. Send one. | | `422` | `source_too_large` | A source's server says the file is over 50 MB. (When it doesn't say, the size is checked during processing instead.) | | `422` | `idempotency_key_reused` | The key was already used for a different endpoint. | | `402` | `insufficient_credits` | Your balance is below 1 credit. | **While processing** (the operation finishes as `failed`, with an `error.code`): | Code | Why | | - | - | | `source_unreachable` | A source URL couldn't be fetched. | | `source_format_unsupported` | A file isn't WAV, MP3 or FLAC audio. | | `source_too_short` | An input is shorter than 1 second. | | `source_too_long` | An input is longer than 5 minutes. | | `source_too_large` | A file is larger than 50 MB. | | `internal_error`, `generation_timeout` | Something went wrong on our side. Resubmit with the same idempotency key. | For a `source_*` error, `error.source` says which input it concerns, `"music"` or `"voice"`, when that is known: ```json theme={null} "error": { "code": "source_unreachable", "message": "The source URL could not be reached.", "source": "voice" } ``` **Source limits for this endpoint:** each input WAV, MP3 or FLAC, from 1 second to 5 minutes long, and at most 50 MB. See [Errors](/errors) for the response shapes, and for the errors every endpoint can return (`401`, `403`, `429`). *** ## Cost **1 credit** per request, normalisation included. Resending a request with the same idempotency key is not charged again. # Normalise loudness Source: https://docs.soundpiece.co/guides/adapt/normalise_loudness Bring a track to a standard loudness for where it will be played `adapt.normalise_loudness` takes a track you already have and delivers it at a target loudness, with its peaks kept safely below clipping. Use it when audio from different places needs to play back at a consistent level: * a podcast episode recorded at a different level from last week's; * music or voiceover that will sit next to other content on a streaming service, a podcast feed or a broadcast channel; * a batch of tracks that should all sound equally loud, one after another. If you are generating or adapting audio with Soundpiece anyway, you may not need this endpoint: `create.new_song`, `create.new_instrumental`, `create.remix_song`, `create.remix_instrumental` and `adapt.auto_cut_down` can normalise their own outputs for free. Add the same `loudness` block to those requests. See [Loudness](/loudness) for how normalisation works on every endpoint. *** ## The request | Field | Required | What it is | | - | - | - | | `source` | yes | The track: `{"url": "https://..."}`, a public link to a WAV, MP3 or FLAC file. | | `loudness` | no | The loudness to deliver at, as a [preset](#presets-or-a-number) or a number. Leave it out for -14 LUFS. | | `idempotency_key` | yes | A UUID that identifies this job. The [reference client](/client) fills it in for you. | | `callback_url` | no | An HTTPS address to receive a [webhook](/webhooks) when the work is finished. | Every field, with its exact rules: [submit](/api-reference/adapt/normalise_loudness/submit) and [poll](/api-reference/adapt/normalise_loudness/poll) in the API Reference. *** ## Example This normalises a podcast episode with the `podcast` preset (-16 LUFS) and saves the result. It uses the [reference client](/client); save `soundpiece.py` or `soundpiece.mjs` next to it first. ```python normalise.py theme={null} import soundpiece result = soundpiece.run("adapt.normalise_loudness", { "source": {"url": "https://example.com/audio/episode.mp3"}, "loudness": {"preset": "podcast"}, }) soundpiece.download(result["output"]["url"], "episode.flac") measured = result["output"]["loudness"] print(f"Saved episode.flac at {measured['lufs']} LUFS, true peak {measured['true_peak_dbtp']} dBTP") ``` ```javascript normalise.mjs theme={null} import { run, download } from "./soundpiece.mjs"; const result = await run("adapt.normalise_loudness", { source: { url: "https://example.com/audio/episode.mp3" }, loudness: { preset: "podcast" }, }); await download(result.output.url, "episode.flac"); const measured = result.output.loudness; console.log(`Saved episode.flac at ${measured.lufs} LUFS, true peak ${measured.true_peak_dbtp} dBTP`); ``` ```bash cURL theme={null} #!/usr/bin/env bash # Submit, wait and download by hand. Needs curl, jq and uuidgen. set -euo pipefail API=https://api.soundpiece.co/v1 AUTH="Authorization: Bearer $SOUNDPIECE_API_KEY" # 1. Submit. Sending this exact body again (same idempotency_key) is safe. body=$(jq -n --arg key "$(uuidgen)" '{ idempotency_key: $key, source: { url: "https://example.com/audio/episode.mp3" }, loudness: { preset: "podcast" } }') response=$(curl --silent --show-error --max-time 30 --request PUT "$API/adapt.normalise_loudness" \ --header "$AUTH" --header "Content-Type: application/json" --data "$body") result=$(jq -e .normalised_audio <<<"$response") || { echo "$response"; exit 1; } id=$(jq -r .id <<<"$result") # 2. Wait. Each call returns as soon as the work is finished, or after 20 seconds. while [ "$(jq -r .status <<<"$result")" = processing ]; do response=$(curl --silent --show-error --max-time 30 "$API/adapt.normalise_loudness?id=$id&wait=true" \ --header "$AUTH") result=$(jq -e .normalised_audio <<<"$response") || { echo "$response"; exit 1; } done # 3. Download, or show what went wrong. if [ "$(jq -r .status <<<"$result")" = failed ]; then jq .error <<<"$result" exit 1 fi curl --silent --show-error --fail --output episode.flac "$(jq -r .output.url <<<"$result")" jq .output.loudness <<<"$result" echo "Saved episode.flac" ``` *** ## The result `run` returns the finished `normalised_audio` object: ```json theme={null} { "id": "op_01ks2p3hc0etfb59tdwh9edd1r", "status": "ready", "created_at": "2026-10-10T09:15:02Z", "idempotency_key": "3f9a1c2e-5b7d-4e8f-a0b1-c2d3e4f5a6b7", "loudness": { "target_lufs": -16.0, "allow_compression": true, "preset": "podcast" }, "output": { "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-10-10T09:31:40Z", "duration": 184.3, "loudness": { "lufs": -16.0, "true_peak_dbtp": -2.1, "compressed": false } }, "error": null } ``` | Field | What it means | | - | - | | `loudness` | What you asked for. `target_lufs` is always filled in, whichever way you asked; `preset` is there only when you named one. | | `output.url` | A signed link to the normalised audio, as a FLAC file (44.1 kHz). It needs no API key. | | `output.expires_at` | When the link stops working. Poll the operation again for a fresh one. | | `output.duration` | The length in seconds. Normalising never changes it. | | `output.loudness.lufs` | The loudness **measured** on the file you download. | | `output.loudness.true_peak_dbtp` | The true peak measured on that file. It is always -1 dBTP or lower. | | `output.loudness.compressed` | `true` if reaching the target meant compressing the dynamics; `false` if only the level changed. | The measurements are taken from the delivered audio, not copied from your request, so they tell you what really happened. The next section shows the three things that can happen. *** ## Three possible outcomes Normalising changes a track's level. Turning a track up raises its loudness and its peaks together, and peaks are never allowed above -1 dBTP. Most audio can reach its target long before its peaks get there. Very dynamic audio cannot: its peaks are loud compared with its average level, so they hit the ceiling first. `allow_compression` decides what happens then. ### 1. It lands on the target Most music and speech, at any sensible target. Only the level changes, and `compressed` is `false`. This is the podcast episode from the example: asked for `{"preset": "podcast"}`, delivered at -16.0 LUFS. ```json theme={null} "loudness": { "target_lufs": -16.0, "allow_compression": true, "preset": "podcast" }, "output": { "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-10-10T09:31:40Z", "duration": 184.3, "loudness": { "lufs": -16.0, "true_peak_dbtp": -2.1, "compressed": false } } ``` ### 2. It is compressed to reach the target A dynamic recording, such as a quiet orchestral piece with loud climaxes, asked to reach -14 LUFS with compression allowed (the default): ```json theme={null} { "source": { "url": "https://example.com/audio/orchestral.wav" }, "loudness": { "target_lufs": -14 } } ``` Turning it up by level alone would push its peaks over the ceiling, so its dynamics are gently compressed. It lands close to the target, typically within about 1.5 LU, and `compressed` is `true`: ```json theme={null} "loudness": { "target_lufs": -14.0, "allow_compression": true }, "output": { "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-10-10T09:31:40Z", "duration": 247.9, "loudness": { "lufs": -14.3, "true_peak_dbtp": -1.0, "compressed": true } } ``` ### 3. It stops short, because you kept the dynamics The same recording, with compression turned off: ```json theme={null} { "source": { "url": "https://example.com/audio/orchestral.wav" }, "loudness": { "target_lufs": -14, "allow_compression": false } } ``` Only the level changes, and it stops when the peaks reach the ceiling. The dynamics are untouched, but the track comes out quieter than you asked for, here at -15.8 LUFS: ```json theme={null} "loudness": { "target_lufs": -14.0, "allow_compression": false }, "output": { "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-10-10T09:31:40Z", "duration": 247.9, "loudness": { "lufs": -15.8, "true_peak_dbtp": -1.0, "compressed": false } } ``` This is not an error: the operation is `ready`, and `lufs` tells you where the track landed. If the difference matters to you, check `lufs` against your target in your own code, and decide whether to accept it or to resubmit with compression allowed (under a new idempotency key, since it is a different request). *** ## Choosing parameters ### Presets or a number There are two ways to say what loudness you want: * **A preset** names a destination: `{"preset": "podcast"}`. Each preset stands for a fixed value that never changes, so your code means the same thing next year. * **A number** gives the target in LUFS, from `-30` to `-6`: `{"target_lufs": -18}`. Use it when a delivery specification gives you a number that isn't a preset. Send one or the other, not both: a block with `preset` and `target_lufs` together is refused with a `422`. Both accept `allow_compression`. An empty block, `{}`, or no block at all, means -14 LUFS with compression allowed. | Preset | Target | What it's for | | - | - | - | | `streaming` | -14 LUFS | Music for streaming services and online video, which most of them play back at about this level. | | `podcast` | -16 LUFS | Podcasts and other spoken-word audio, the level podcast platforms recommend. | | `broadcast` | -23 LUFS | Television and radio broadcast, the EBU R128 standard. | ### Choosing a target by destination | Where the audio is going | Send | | - | - | | Music streaming services, online video, social media | `{"preset": "streaming"}` (or leave `loudness` out) | | A podcast feed, an audiobook sample, a voice-led explainer | `{"preset": "podcast"}` | | European TV or radio (EBU R128) | `{"preset": "broadcast"}` | | A client's delivery specification | Their number, as `target_lufs` | If you don't know where the audio will end up, `streaming` is a safe default. Audio that is louder than a platform's playback level is turned down by the platform, and audio that is quieter sounds weak next to everything else. ### Should I allow compression? * **Leave it on (the default)** when hitting the number matters most: a platform or client will measure the delivery, or it will play right after other audio at that level. * **Turn it off** when the dynamics matter more than the number: classical music, film score, or anything where the quiet parts should stay quiet. Expect very dynamic audio to come out a little below the target. For most material it makes no difference, because the target can be reached by a change of level alone. ### The peak ceiling The true peak is always kept at or below -1 dBTP, and that isn't a setting. It leaves the headroom that MP3 or AAC encoding and playback need to stay free of clipping. *** ## Errors **When you submit** (the request is refused, nothing is charged): | Status | `type` | Why | | - | - | - | | `422` | `extra_forbidden` | `loudness` has both `preset` and `target_lufs`. Send one. | | `422` | `literal_error` | `preset` isn't one of `streaming`, `podcast` or `broadcast`. | | `422` | `less_than_equal` / `greater_than_equal` | `target_lufs` is outside `-30` to `-6`. | | `422` | `source_too_large` | The source's server says the file is over 50 MB. (When it doesn't say, the size is checked during processing instead.) | | `422` | `idempotency_key_reused` | The key was already used for a different endpoint. | | `402` | `insufficient_credits` | Your balance is below 1 credit. | **While processing** (the operation finishes as `failed`, with an `error.code`): | Code | Why | | - | - | | `source_unreachable` | The source URL couldn't be fetched. | | `source_format_unsupported` | The file isn't WAV, MP3 or FLAC audio. | | `source_too_short` | The audio is shorter than 1 second. | | `source_too_long` | The audio is longer than 5 minutes. | | `source_too_large` | The file is larger than 50 MB. | | `internal_error`, `generation_timeout` | Something went wrong on our side. Resubmit with the same idempotency key. | **Source limits for this endpoint:** WAV, MP3 or FLAC, from 1 second to 5 minutes long, and at most 50 MB. See [Errors](/errors) for the response shapes, and for the errors every endpoint can return (`401`, `403`, `429`). *** ## Cost **1 credit** per request, whatever the target and however long the track. Resending a request with the same idempotency key is not charged again. # Separate stems Source: https://docs.soundpiece.co/guides/adapt/separate_stems Split a mixed track into vocals, drums, bass, other and instrumental `adapt.separate_stems` takes a finished, mixed track and splits it into its parts ("stems"), each as its own file. Use it to make an instrumental or a vocal-only version of a song, to remix or re-balance a track, or to pull out a drum loop. *** ## The request | Field | Required | What it is | | - | - | - | | `source` | yes | The track: `{"url": "https://..."}`, a public link to a WAV, MP3 or FLAC file. | | `idempotency_key` | yes | A UUID that identifies this job. The [reference client](/client) fills it in for you. | | `callback_url` | no | An HTTPS address to receive a [webhook](/webhooks) when the work is finished. | There is nothing else to choose: every request returns all five stems. Every field, with its exact rules: [submit](/api-reference/adapt/separate_stems/submit) and [poll](/api-reference/adapt/separate_stems/poll) in the API Reference. *** ## Example This separates a song and saves each stem as `.flac`. It uses the [reference client](/client); save `soundpiece.py` or `soundpiece.mjs` next to it first. ```python stems.py theme={null} import soundpiece stems = soundpiece.run("adapt.separate_stems", { "source": {"url": "https://example.com/audio/song.mp3"}, }) for stem in stems["outputs"]: soundpiece.download(stem["url"], f"{stem['kind']}.flac") print(f"Saved {stem['kind']}.flac") ``` ```javascript stems.mjs theme={null} import { run, download } from "./soundpiece.mjs"; const stems = await run("adapt.separate_stems", { source: { url: "https://example.com/audio/song.mp3" }, }); for (const stem of stems.outputs) { await download(stem.url, `${stem.kind}.flac`); console.log(`Saved ${stem.kind}.flac`); } ``` ```bash cURL theme={null} #!/usr/bin/env bash # Submit, wait and download by hand. Needs curl, jq and uuidgen. set -euo pipefail API=https://api.soundpiece.co/v1 AUTH="Authorization: Bearer $SOUNDPIECE_API_KEY" # 1. Submit. Sending this exact body again (same idempotency_key) is safe. body=$(jq -n --arg key "$(uuidgen)" '{ idempotency_key: $key, source: { url: "https://example.com/audio/song.mp3" } }') response=$(curl --silent --show-error --max-time 30 --request PUT "$API/adapt.separate_stems" \ --header "$AUTH" --header "Content-Type: application/json" --data "$body") result=$(jq -e .stems <<<"$response") || { echo "$response"; exit 1; } id=$(jq -r .id <<<"$result") # 2. Wait. Each call returns as soon as the work is finished, or after 20 seconds. while [ "$(jq -r .status <<<"$result")" = processing ]; do response=$(curl --silent --show-error --max-time 30 "$API/adapt.separate_stems?id=$id&wait=true" \ --header "$AUTH") result=$(jq -e .stems <<<"$response") || { echo "$response"; exit 1; } done # 3. Download each stem, or show what went wrong. if [ "$(jq -r .status <<<"$result")" = failed ]; then jq .error <<<"$result" exit 1 fi jq -r '.outputs[] | "\(.kind) \(.url)"' <<<"$result" | while read -r kind url; do curl --silent --show-error --fail --output "$kind.flac" "$url" echo "Saved $kind.flac" done ``` *** ## The result `run` returns the finished `stems` object, with one entry in `outputs` per stem: ```json theme={null} { "id": "op_01ks2q4m7rfx2b9c3d5e8g1h6j", "status": "ready", "created_at": "2026-10-10T09:15:02Z", "idempotency_key": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f7a", "outputs": [ { "kind": "vocals", "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-10-10T09:31:40Z", "duration": 201.4 }, { "kind": "drums", "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-10-10T09:31:40Z", "duration": 201.4 }, { "kind": "bass", "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-10-10T09:31:40Z", "duration": 201.4 }, { "kind": "other", "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-10-10T09:31:40Z", "duration": 201.4 }, { "kind": "instrumental", "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-10-10T09:31:40Z", "duration": 201.4 } ], "error": null } ``` | Field | What it means | | - | - | | `outputs[].kind` | Which stem the file is (see below). Find a stem by its `kind`, not by its position in the list. | | `outputs[].url` | A signed link to the stem, as a FLAC file (44.1 kHz). It needs no API key. | | `outputs[].expires_at` | When the link stops working. Poll the operation again for a fresh one. | | `outputs[].duration` | The stem's length in seconds, the same as the source's. | | `kind` | What's in it | | - | - | | `vocals` | The lead and backing vocals on their own. | | `drums` | The drums and percussion. | | `bass` | The bassline. | | `other` | Everything else: guitars, keys, synths and other melodic instruments. | | `instrumental` | The whole track without the vocals (everything except `vocals`, together). | *** ## Choosing parameters There are none to choose, but a few things help: * **Start from the best copy you have.** A WAV or FLAC separates more cleanly than a heavily compressed MP3. * **Want only an instrumental?** Download just the `instrumental` stem; it is already the track minus its vocals, so you don't need to mix the others back together. * **Want new music in the same style instead?** Look at [`create.remix_song`](/guides/create/remix_song) or [`create.remix_instrumental`](/guides/create/remix_instrumental). *** ## Errors **When you submit** (the request is refused, nothing is charged): | Status | `type` | Why | | - | - | - | | `422` | `source_too_large` | The source's server says the file is over 50 MB. (When it doesn't say, the size is checked during processing instead.) | | `422` | `idempotency_key_reused` | The key was already used for a different endpoint. | | `402` | `insufficient_credits` | Your balance is below 3 credits. | **While processing** (the operation finishes as `failed`, with an `error.code`): | Code | Why | | - | - | | `source_unreachable` | The source URL couldn't be fetched. | | `source_format_unsupported` | The file isn't WAV, MP3 or FLAC audio. | | `source_too_short` | The audio is shorter than 1 second. | | `source_too_long` | The audio is longer than 5 minutes. | | `source_too_large` | The file is larger than 50 MB. | | `internal_error`, `generation_timeout` | Something went wrong on our side. Resubmit with the same idempotency key. | **Source limits for this endpoint:** WAV, MP3 or FLAC, from 1 second to 5 minutes long, and at most 50 MB. See [Errors](/errors) for the response shapes, and for the errors every endpoint can return (`401`, `403`, `429`). *** ## Cost **3 credits** per request, for all five stems. Resending a request with the same idempotency key is not charged again. # New sound effect Source: https://docs.soundpiece.co/guides/create/new_fx Generate a one-shot sound effect from a description `create.new_fx` makes a single sound effect from a description: a riser, an impact, a whoosh, a transition, or everyday foley like footsteps or a keyboard. Use it for video edits, games, apps and adverts. For music rather than a sound, use [`create.new_sample`](/guides/create/new_sample). *** ## The request | Field | Required | What it is | | - | - | - | | `prompt` | yes | A description of the sound, up to 450 characters, e.g. `"rising whoosh transition"`. | | `duration` | yes | The length you want, in seconds. | | `idempotency_key` | yes | A UUID that identifies this job. The [reference client](/client) fills it in for you. | | `callback_url` | no | An HTTPS address to receive a [webhook](/webhooks) when the work is finished. | Every field, with its exact rules: [submit](/api-reference/create/new_fx/submit) and [poll](/api-reference/create/new_fx/poll) in the API Reference. *** ## Example This makes a 3-second cinematic impact and saves it. It uses the [reference client](/client); save `soundpiece.py` or `soundpiece.mjs` next to it first. ```python fx.py theme={null} import soundpiece fx = soundpiece.run("create.new_fx", { "prompt": "a deep cinematic impact with a long metallic tail", "duration": 3, }) soundpiece.download(fx["output"]["url"], "impact.flac") print("Saved impact.flac") ``` ```javascript fx.mjs theme={null} import { run, download } from "./soundpiece.mjs"; const fx = await run("create.new_fx", { prompt: "a deep cinematic impact with a long metallic tail", duration: 3, }); await download(fx.output.url, "impact.flac"); console.log("Saved impact.flac"); ``` ```bash cURL theme={null} #!/usr/bin/env bash # Submit, wait and download by hand. Needs curl, jq and uuidgen. set -euo pipefail API=https://api.soundpiece.co/v1 AUTH="Authorization: Bearer $SOUNDPIECE_API_KEY" # 1. Submit. Sending this exact body again (same idempotency_key) is safe. body=$(jq -n --arg key "$(uuidgen)" '{ idempotency_key: $key, prompt: "a deep cinematic impact with a long metallic tail", duration: 3 }') response=$(curl --silent --show-error --max-time 30 --request PUT "$API/create.new_fx" \ --header "$AUTH" --header "Content-Type: application/json" --data "$body") result=$(jq -e .fx <<<"$response") || { echo "$response"; exit 1; } id=$(jq -r .id <<<"$result") # 2. Wait. Each call returns as soon as the work is finished, or after 20 seconds. while [ "$(jq -r .status <<<"$result")" = processing ]; do response=$(curl --silent --show-error --max-time 30 "$API/create.new_fx?id=$id&wait=true" \ --header "$AUTH") result=$(jq -e .fx <<<"$response") || { echo "$response"; exit 1; } done # 3. Download, or show what went wrong. if [ "$(jq -r .status <<<"$result")" = failed ]; then jq .error <<<"$result" exit 1 fi curl --silent --show-error --fail --output impact.flac "$(jq -r .output.url <<<"$result")" echo "Saved impact.flac" ``` *** ## The result `run` returns the finished `fx` object: ```json theme={null} { "id": "op_01ks2qgr1vj06e2f8g9h1k4m7n", "status": "ready", "created_at": "2026-10-10T09:15:02Z", "idempotency_key": "a7b8c9d0-e1f2-4a3b-8c4d-5e6f7a8b9c0d", "prompt": "a deep cinematic impact with a long metallic tail", "duration": 3.0, "output": { "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-10-10T09:31:40Z", "duration": 3.0 }, "error": null } ``` | Field | What it means | | - | - | | `prompt`, `duration` | What you asked for. | | `output.url` | A signed link to the effect, as a FLAC file (44.1 kHz). It needs no API key. | | `output.expires_at` | When the link stops working. Poll the operation again for a fresh one. | | `output.duration` | The effect's length in seconds. | *** ## Choosing parameters * **Prompt.** Describe the sound itself: what makes it, what happens, and where. `"footsteps on gravel, slow, outdoors"` works better than `"walking sound"`. Keep it under 450 characters. * **Duration.** Match the moment it's for: a second or two for an impact or a click, a few seconds for a riser or a transition. * **Loudness.** This endpoint doesn't take a `loudness` block. To normalise an effect, pass its URL to [`adapt.normalise_loudness`](/guides/adapt/normalise_loudness). *** ## Errors **When you submit** (the request is refused, nothing is charged): | Status | `type` | Why | | - | - | - | | `422` | `string_too_short` / `string_too_long` | `prompt` is empty or over 450 characters. | | `422` | `missing` | `prompt` or `duration` is missing. | | `422` | `idempotency_key_reused` | The key was already used for a different endpoint. | | `402` | `insufficient_credits` | Your balance is below 3 credits. | **While processing** (the operation finishes as `failed`, with an `error.code`): | Code | Why | | - | - | | `tos_violation` | The prompt was rejected by our content policy. | | `internal_error`, `generation_timeout` | Something went wrong on our side. Resubmit with the same idempotency key. | This endpoint takes no source audio, so it has no source limits and none of the `source_*` errors. See [Errors](/errors) for the response shapes, and for the errors every endpoint can return (`401`, `403`, `429`). *** ## Cost **3 credits** per request. Resending a request with the same idempotency key is not charged again. # New instrumental Source: https://docs.soundpiece.co/guides/create/new_instrumental Generate an instrumental from a prompt or a reference track `create.new_instrumental` makes a full piece of music with no vocals. You describe the sound in words (a `prompt`) or point at a track whose style to follow (a `reference`). Use it for background music, beds for voiceover, hold music, game and app soundtracks. For a song with sung lyrics, use [`create.new_song`](/guides/create/new_song). For a short clip at an exact length, use [`create.new_sample`](/guides/create/new_sample). *** ## The request | Field | Required | What it is | | - | - | - | | `prompt` | one of these two | A description of the sound, up to 1,024 characters, e.g. `"synthwave, melancholic, mid-tempo"`. | | `reference` | one of these two | A track whose style to follow: `{"url": "https://..."}`, a public link to a WAV, MP3 or FLAC file. | | `n_variations` | no | How many different pieces to make from the same request, from 1 to 3. Defaults to 1. | | `loudness` | no | Normalise every piece to a target loudness, at no extra cost. Leave it out to get them as produced. See [Loudness](/loudness). | | `idempotency_key` | yes | A UUID that identifies this job. The [reference client](/client) fills it in for you. | | `callback_url` | no | An HTTPS address to receive a [webhook](/webhooks) when the work is finished. | Send `prompt` or `reference`, not both. Every field, with its exact rules: [submit](/api-reference/create/new_instrumental/submit) and [poll](/api-reference/create/new_instrumental/poll) in the API Reference. *** ## Example This makes an instrumental in the style of a reference track and saves it. It uses the [reference client](/client); save `soundpiece.py` or `soundpiece.mjs` next to it first. ```python instrumental.py theme={null} import soundpiece instrumental = soundpiece.run("create.new_instrumental", { "reference": {"url": "https://example.com/audio/reference.mp3"}, }) soundpiece.download(instrumental["outputs"][0]["url"], "instrumental.flac") print("Saved instrumental.flac") ``` ```javascript instrumental.mjs theme={null} import { run, download } from "./soundpiece.mjs"; const instrumental = await run("create.new_instrumental", { reference: { url: "https://example.com/audio/reference.mp3" }, }); await download(instrumental.outputs[0].url, "instrumental.flac"); console.log("Saved instrumental.flac"); ``` ```bash cURL theme={null} #!/usr/bin/env bash # Submit, wait and download by hand. Needs curl, jq and uuidgen. set -euo pipefail API=https://api.soundpiece.co/v1 AUTH="Authorization: Bearer $SOUNDPIECE_API_KEY" # 1. Submit. Sending this exact body again (same idempotency_key) is safe. body=$(jq -n --arg key "$(uuidgen)" '{ idempotency_key: $key, reference: { url: "https://example.com/audio/reference.mp3" } }') response=$(curl --silent --show-error --max-time 30 --request PUT "$API/create.new_instrumental" \ --header "$AUTH" --header "Content-Type: application/json" --data "$body") result=$(jq -e .instrumental <<<"$response") || { echo "$response"; exit 1; } id=$(jq -r .id <<<"$result") # 2. Wait. Each call returns as soon as the work is finished, or after 20 seconds. while [ "$(jq -r .status <<<"$result")" = processing ]; do response=$(curl --silent --show-error --max-time 30 "$API/create.new_instrumental?id=$id&wait=true" \ --header "$AUTH") result=$(jq -e .instrumental <<<"$response") || { echo "$response"; exit 1; } done # 3. Download, or show what went wrong. if [ "$(jq -r .status <<<"$result")" = failed ]; then jq .error <<<"$result" exit 1 fi curl --silent --show-error --fail --output instrumental.flac "$(jq -r '.outputs[0].url' <<<"$result")" echo "Saved instrumental.flac" ``` *** ## The result `run` returns the finished `instrumental` object, with one entry in `outputs` per variation: ```json theme={null} { "id": "op_01ks2q8n3sgy4c0d6e7f9h2j5k", "status": "ready", "created_at": "2026-10-10T09:15:02Z", "idempotency_key": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a8b", "prompt": null, "n_variations": 1, "outputs": [ { "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-10-10T09:31:40Z", "duration": 134.8 } ], "error": null } ``` | Field | What it means | | - | - | | `prompt` | The prompt you sent, or `null` when you sent a `reference` (as here). The reference URL is never echoed. | | `n_variations` | How many pieces you asked for. | | `loudness` | Present only if you asked for normalisation (not in this example). | | `outputs[].url` | A signed link to one piece, as a FLAC file (44.1 kHz). It needs no API key. | | `outputs[].expires_at` | When the link stops working. Poll the operation again for a fresh one. | | `outputs[].duration` | The piece's length in seconds. There is no length setting; to fit a length, cut the result with [`adapt.auto_cut_down`](/guides/adapt/auto_cut_down). | | `outputs[].loudness` | Present only if you asked for normalisation: `lufs`, `true_peak_dbtp` and `compressed`, measured on that piece. | *** ## Choosing parameters * **Prompt or reference?** A prompt is quickest: mix genre, mood, tempo and instruments, separated by commas. A reference is better when you have a track whose feel you want to match. To use a prompt, send it in place of `reference`: ```json theme={null} { "prompt": "synthwave, melancholic, mid-tempo", "n_variations": 3, "loudness": { "preset": "streaming" } } ``` * **`n_variations`.** Each variation is a different take on the same request. Ask for two or three when you want to choose; each one costs the same as a single piece. * **Loudness.** Off unless you include the block. Under a voiceover, consider [`adapt.auto_duck`](/guides/adapt/auto_duck) instead, which balances the music against the voice for you. See the [loudness normalisation guide](/guides/adapt/normalise_loudness#choosing-a-target-by-destination) for which target to pick. *** ## Errors **When you submit** (the request is refused, nothing is charged): | Status | `type` | Why | | - | - | - | | `422` | `string_too_short` / `string_too_long` | `prompt` is empty or over 1,024 characters. | | `422` | `missing` | Neither `prompt` nor `reference` was sent. | | `422` | `extra_forbidden` | Both `prompt` and `reference` were sent (or any field the endpoint doesn't know). | | `422` | `source_too_large` | The reference's server says the file is over 50 MB. (When it doesn't say, the size is checked during processing instead.) | | `422` | `idempotency_key_reused` | The key was already used for a different endpoint. | | `402` | `insufficient_credits` | Your balance is below 3 credits per variation. | **While processing** (the operation finishes as `failed`, with an `error.code`): | Code | Why | | - | - | | `tos_violation` | The prompt or reference was rejected by our content policy. | | `source_unreachable`, `source_format_unsupported`, `source_too_short`, `source_too_long`, `source_too_large` | Only with a `reference`: it couldn't be fetched, isn't WAV, MP3 or FLAC audio, or is outside the limits below. | | `internal_error`, `generation_timeout` | Something went wrong on our side. Resubmit with the same idempotency key. | **Reference limits for this endpoint:** WAV, MP3 or FLAC, from 1 second to 5 minutes long, and at most 50 MB. See [Errors](/errors) for the response shapes, and for the errors every endpoint can return (`401`, `403`, `429`). *** ## Cost **3 credits per variation**, so `n_variations: 3` costs 9. Normalisation costs nothing extra. Resending a request with the same idempotency key is not charged again. # New sample Source: https://docs.soundpiece.co/guides/create/new_sample Generate a short music clip at the length you choose, optionally loopable `create.new_sample` makes a short piece of music (a "sample") from a description, at the length you ask for. Set `loopable` to make a clip that repeats seamlessly, end to start. Use it for loops in a music app, stings and idents, short backgrounds for video, or building blocks for your own tracks. Samples have no vocals. For a full-length piece, use [`create.new_instrumental`](/guides/create/new_instrumental). For a sound rather than music (a whoosh, an impact, a door slam), use [`create.new_fx`](/guides/create/new_fx). *** ## The request | Field | Required | What it is | | - | - | - | | `prompt` | yes | A description of the music, up to 1,024 characters, e.g. `"rock, guitar, energetic, no drums"`. | | `duration` | yes | The length you want, in seconds. | | `loopable` | no | `true` to make a clip designed to repeat seamlessly. Defaults to `false`. | | `idempotency_key` | yes | A UUID that identifies this job. The [reference client](/client) fills it in for you. | | `callback_url` | no | An HTTPS address to receive a [webhook](/webhooks) when the work is finished. | Every field, with its exact rules: [submit](/api-reference/create/new_sample/submit) and [poll](/api-reference/create/new_sample/poll) in the API Reference. *** ## Example This makes an 8-second guitar loop and saves it. It uses the [reference client](/client); save `soundpiece.py` or `soundpiece.mjs` next to it first. ```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"); ``` ```bash cURL theme={null} #!/usr/bin/env bash # Submit, wait and download by hand. Needs curl, jq and uuidgen. set -euo pipefail API=https://api.soundpiece.co/v1 AUTH="Authorization: Bearer $SOUNDPIECE_API_KEY" # 1. Submit. Sending this exact body again (same idempotency_key) is safe. body=$(jq -n --arg key "$(uuidgen)" '{ idempotency_key: $key, prompt: "rock, guitar, energetic, no drums", duration: 8, loopable: true }') response=$(curl --silent --show-error --max-time 30 --request PUT "$API/create.new_sample" \ --header "$AUTH" --header "Content-Type: application/json" --data "$body") result=$(jq -e .sample <<<"$response") || { echo "$response"; exit 1; } id=$(jq -r .id <<<"$result") # 2. Wait. Each call returns as soon as the work is finished, or after 20 seconds. while [ "$(jq -r .status <<<"$result")" = processing ]; do response=$(curl --silent --show-error --max-time 30 "$API/create.new_sample?id=$id&wait=true" \ --header "$AUTH") result=$(jq -e .sample <<<"$response") || { echo "$response"; exit 1; } done # 3. Download, or show what went wrong. if [ "$(jq -r .status <<<"$result")" = failed ]; then jq .error <<<"$result" exit 1 fi curl --silent --show-error --fail --output sample.flac "$(jq -r .output.url <<<"$result")" echo "Saved sample.flac" ``` *** ## The result `run` returns the finished `sample` object: ```json theme={null} { "id": "op_01ks2qcp9thz5d1e7f8g0j3k6m", "status": "ready", "created_at": "2026-10-10T09:15:02Z", "idempotency_key": "f6a7b8c9-d0e1-4f2a-9b3c-4d5e6f7a8b9c", "prompt": "rock, guitar, energetic, no drums", "duration": 8.0, "loopable": true, "output": { "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-10-10T09:31:40Z", "duration": 8.0 }, "error": null } ``` | Field | What it means | | - | - | | `prompt`, `duration`, `loopable` | What you asked for. | | `output.url` | A signed link to the sample, as a FLAC file (44.1 kHz). It needs no API key. | | `output.expires_at` | When the link stops working. Poll the operation again for a fresh one. | | `output.duration` | The sample's length in seconds. | *** ## Choosing parameters * **Prompt.** Mix genre, mood and instruments, separated by commas. Say what you *don't* want too, e.g. `"no drums"`, when the sample will be layered with other parts. * **Duration.** For a loop, choose a length that holds a whole number of bars at your tempo: at 120 BPM in 4/4, one bar is 2 seconds, so 8 seconds is four bars. * **`loopable`.** Turn it on when the clip will repeat, so the end flows back into the start without a click or a jump. Leave it off for a one-off sting or ident. * **Loudness.** This endpoint doesn't take a `loudness` block. To normalise a sample, pass its URL to [`adapt.normalise_loudness`](/guides/adapt/normalise_loudness). *** ## Errors **When you submit** (the request is refused, nothing is charged): | Status | `type` | Why | | - | - | - | | `422` | `string_too_short` / `string_too_long` | `prompt` is empty or over 1,024 characters. | | `422` | `missing` | `prompt` or `duration` is missing. | | `422` | `idempotency_key_reused` | The key was already used for a different endpoint. | | `402` | `insufficient_credits` | Your balance is below 3 credits. | **While processing** (the operation finishes as `failed`, with an `error.code`): | Code | Why | | - | - | | `tos_violation` | The prompt was rejected by our content policy. | | `internal_error`, `generation_timeout` | Something went wrong on our side. Resubmit with the same idempotency key. | This endpoint takes no source audio, so it has no source limits and none of the `source_*` errors. See [Errors](/errors) for the response shapes, and for the errors every endpoint can return (`401`, `403`, `429`). *** ## Cost **3 credits** per request. Resending a request with the same idempotency key is not charged again. # New song Source: https://docs.soundpiece.co/guides/create/new_song Generate a song with vocals from your lyrics and a prompt or a reference track `create.new_song` writes and performs a complete song, with vocals singing the lyrics you give it. You describe the sound either in words (a `prompt`) or by pointing at a track whose style to follow (a `reference`). Use it for jingles, personalised songs, demo tracks and anything else that needs a sung vocal. For music without vocals, use [`create.new_instrumental`](/guides/create/new_instrumental). *** ## The request | Field | Required | What it is | | - | - | - | | `lyrics` | yes | The words to sing, up to 3,000 characters. Structure them with section markers such as `[Verse]` and `[Chorus]`. | | `prompt` | one of these two | A description of the sound, up to 1,024 characters, e.g. `"synthwave, melancholic, mid-tempo"`. | | `reference` | one of these two | A track whose style to follow: `{"url": "https://..."}`, a public link to a WAV, MP3 or FLAC file. | | `n_variations` | no | How many different songs to make from the same request, from 1 to 3. Defaults to 1. | | `loudness` | no | Normalise every song to a target loudness, at no extra cost. Leave it out to get them as produced. See [Loudness](/loudness). | | `idempotency_key` | yes | A UUID that identifies this job. The [reference client](/client) fills it in for you. | | `callback_url` | no | An HTTPS address to receive a [webhook](/webhooks) when the work is finished. | Send `prompt` or `reference`, not both. Every field, with its exact rules: [submit](/api-reference/create/new_song/submit) and [poll](/api-reference/create/new_song/poll) in the API Reference. *** ## Example This makes two versions of a song from a prompt, both normalised for streaming, and saves them. It uses the [reference client](/client); save `soundpiece.py` or `soundpiece.mjs` next to it first. ```python song.py theme={null} import soundpiece song = soundpiece.run("create.new_song", { "lyrics": "[Verse]\nDriving all night with the radio on\n\n[Chorus]\nCity lights, carry me home", "prompt": "synthwave, melancholic, mid-tempo", "n_variations": 2, "loudness": {"preset": "streaming"}, }) for number, output in enumerate(song["outputs"], start=1): soundpiece.download(output["url"], f"song-{number}.flac") print(f"Saved song-{number}.flac") ``` ```javascript song.mjs theme={null} import { run, download } from "./soundpiece.mjs"; const song = await run("create.new_song", { lyrics: "[Verse]\nDriving all night with the radio on\n\n[Chorus]\nCity lights, carry me home", prompt: "synthwave, melancholic, mid-tempo", n_variations: 2, loudness: { preset: "streaming" }, }); for (const [index, output] of song.outputs.entries()) { await download(output.url, `song-${index + 1}.flac`); console.log(`Saved song-${index + 1}.flac`); } ``` ```bash cURL theme={null} #!/usr/bin/env bash # Submit, wait and download by hand. Needs curl, jq and uuidgen. set -euo pipefail API=https://api.soundpiece.co/v1 AUTH="Authorization: Bearer $SOUNDPIECE_API_KEY" # 1. Submit. Sending this exact body again (same idempotency_key) is safe. body=$(jq -n --arg key "$(uuidgen)" '{ idempotency_key: $key, lyrics: "[Verse]\nDriving all night with the radio on\n\n[Chorus]\nCity lights, carry me home", prompt: "synthwave, melancholic, mid-tempo", n_variations: 2, loudness: { preset: "streaming" } }') response=$(curl --silent --show-error --max-time 30 --request PUT "$API/create.new_song" \ --header "$AUTH" --header "Content-Type: application/json" --data "$body") result=$(jq -e .song <<<"$response") || { echo "$response"; exit 1; } id=$(jq -r .id <<<"$result") # 2. Wait. Each call returns as soon as the work is finished, or after 20 seconds. while [ "$(jq -r .status <<<"$result")" = processing ]; do response=$(curl --silent --show-error --max-time 30 "$API/create.new_song?id=$id&wait=true" \ --header "$AUTH") result=$(jq -e .song <<<"$response") || { echo "$response"; exit 1; } done # 3. Download each song, or show what went wrong. if [ "$(jq -r .status <<<"$result")" = failed ]; then jq .error <<<"$result" exit 1 fi number=0 for url in $(jq -r '.outputs[].url' <<<"$result"); do number=$((number + 1)) curl --silent --show-error --fail --output "song-$number.flac" "$url" echo "Saved song-$number.flac" done ``` *** ## The result `run` returns the finished `song` object, with one entry in `outputs` per variation: ```json theme={null} { "id": "op_01ks2p3hc0etfb59tdwh9edd1r", "status": "ready", "created_at": "2026-10-10T09:15:02Z", "idempotency_key": "550e8400-e29b-41d4-a716-446655440000", "lyrics": "[Verse]\nDriving all night with the radio on\n\n[Chorus]\nCity lights, carry me home", "prompt": "synthwave, melancholic, mid-tempo", "n_variations": 2, "loudness": { "target_lufs": -14.0, "allow_compression": true, "preset": "streaming" }, "outputs": [ { "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-10-10T09:31:40Z", "duration": 87.4, "loudness": { "lufs": -14.0, "true_peak_dbtp": -1.3, "compressed": false } }, { "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-10-10T09:31:40Z", "duration": 92.1, "loudness": { "lufs": -14.1, "true_peak_dbtp": -1.0, "compressed": true } } ], "error": null } ``` | Field | What it means | | - | - | | `lyrics`, `prompt`, `n_variations` | What you asked for. `prompt` is `null` when you sent a `reference` instead; the reference URL is never echoed. | | `loudness` | Present only if you asked for normalisation: the target, and `preset` when you named one. | | `outputs[].url` | A signed link to one song, as a FLAC file (44.1 kHz). It needs no API key. | | `outputs[].expires_at` | When the link stops working. Poll the operation again for a fresh one. | | `outputs[].duration` | The song's length in seconds. There is no length setting: it follows from the lyrics and the style. | | `outputs[].loudness` | Present only if you asked for normalisation: `lufs`, `true_peak_dbtp` and `compressed`, measured on that song. | *** ## Choosing parameters * **Lyrics.** Put each section marker (`[Intro]`, `[Verse]`, `[Chorus]`, `[Bridge]`, `[Outro]`, `[Instrumental]`) on its own line, and each sung line on its own line under it. * **Prompt or reference?** A prompt is quickest: mix genre, mood, tempo and instruments, separated by commas, e.g. `"rock song, energetic, male vocal"`. A reference is better when you have a track whose feel you want to match. To use one, send it in place of `prompt`: ```json theme={null} { "lyrics": "[Verse]\nDriving all night with the radio on\n\n[Chorus]\nCity lights, carry me home", "reference": { "url": "https://example.com/audio/reference.mp3" } } ``` * **`n_variations`.** Each variation is a different take on the same request. Ask for two or three when you want to pick the best; each one costs the same as a single song. * **Loudness.** Off unless you include the block. Add `{"preset": "streaming"}` (or `podcast`, `broadcast`, or a `target_lufs`) when the songs go straight to a platform. See the [loudness normalisation guide](/guides/adapt/normalise_loudness#choosing-a-target-by-destination). *** ## Errors **When you submit** (the request is refused, nothing is charged): | Status | `type` | Why | | - | - | - | | `422` | `string_too_short` / `string_too_long` | `lyrics` is empty or over 3,000 characters, or `prompt` is empty or over 1,024. | | `422` | `missing` | `lyrics` is missing, or neither `prompt` nor `reference` was sent. | | `422` | `extra_forbidden` | Both `prompt` and `reference` were sent (or any field the endpoint doesn't know). | | `422` | `source_too_large` | The reference's server says the file is over 50 MB. (When it doesn't say, the size is checked during processing instead.) | | `422` | `idempotency_key_reused` | The key was already used for a different endpoint. | | `402` | `insufficient_credits` | Your balance is below 3 credits per variation. | **While processing** (the operation finishes as `failed`, with an `error.code`): | Code | Why | | - | - | | `tos_violation` | The lyrics, prompt or reference were rejected by our content policy. | | `source_unreachable`, `source_format_unsupported`, `source_too_short`, `source_too_long`, `source_too_large` | Only with a `reference`: it couldn't be fetched, isn't WAV, MP3 or FLAC audio, or is outside the limits below. | | `internal_error`, `generation_timeout` | Something went wrong on our side. Resubmit with the same idempotency key. | **Reference limits for this endpoint:** WAV, MP3 or FLAC, from 1 second to 5 minutes long, and at most 50 MB. See [Errors](/errors) for the response shapes, and for the errors every endpoint can return (`401`, `403`, `429`). *** ## Cost **3 credits per variation**, so `n_variations: 2` costs 6. Normalisation costs nothing extra. Resending a request with the same idempotency key is not charged again. # Remix an instrumental Source: https://docs.soundpiece.co/guides/create/remix_instrumental Make new variations of an instrumental track, optionally steered by a prompt `create.remix_instrumental` takes a piece of music with no vocals and makes new variations of it. On its own, the source guides the result; add a `prompt` to steer it toward a style or mood you describe. Use it to get fresh versions of a music bed, to vary a theme across a series, or to restyle a track you already own. For a track with vocals, use [`create.remix_song`](/guides/create/remix_song). *** ## The request | Field | Required | What it is | | - | - | - | | `source` | yes | The instrumental to remix: `{"url": "https://..."}`, a public link to a WAV, MP3 or FLAC file. | | `prompt` | no | The style or mood to steer toward, up to 1,024 characters, as a short phrase, e.g. `"Dreamy, melancholic synthwave, mid-tempo"`. | | `n_variations` | no | How many variations to make, from 1 to 3. Defaults to 1. | | `loudness` | no | Normalise every variation to a target loudness, at no extra cost. Leave it out to get them as produced. See [Loudness](/loudness). | | `idempotency_key` | yes | A UUID that identifies this job. The [reference client](/client) fills it in for you. | | `callback_url` | no | An HTTPS address to receive a [webhook](/webhooks) when the work is finished. | Every field, with its exact rules: [submit](/api-reference/create/remix_instrumental/submit) and [poll](/api-reference/create/remix_instrumental/poll) in the API Reference. *** ## Example This makes three variations of an instrumental, steered toward synthwave, and saves them. It uses the [reference client](/client); save `soundpiece.py` or `soundpiece.mjs` next to it first. ```python remix.py theme={null} import soundpiece remix = soundpiece.run("create.remix_instrumental", { "source": {"url": "https://example.com/audio/instrumental.mp3"}, "prompt": "Dreamy, melancholic synthwave, mid-tempo", "n_variations": 3, }) for number, output in enumerate(remix["outputs"], start=1): soundpiece.download(output["url"], f"variation-{number}.flac") print(f"Saved variation-{number}.flac") ``` ```javascript remix.mjs theme={null} import { run, download } from "./soundpiece.mjs"; const remix = await run("create.remix_instrumental", { source: { url: "https://example.com/audio/instrumental.mp3" }, prompt: "Dreamy, melancholic synthwave, mid-tempo", n_variations: 3, }); for (const [index, output] of remix.outputs.entries()) { await download(output.url, `variation-${index + 1}.flac`); console.log(`Saved variation-${index + 1}.flac`); } ``` ```bash cURL theme={null} #!/usr/bin/env bash # Submit, wait and download by hand. Needs curl, jq and uuidgen. set -euo pipefail API=https://api.soundpiece.co/v1 AUTH="Authorization: Bearer $SOUNDPIECE_API_KEY" # 1. Submit. Sending this exact body again (same idempotency_key) is safe. body=$(jq -n --arg key "$(uuidgen)" '{ idempotency_key: $key, source: { url: "https://example.com/audio/instrumental.mp3" }, prompt: "Dreamy, melancholic synthwave, mid-tempo", n_variations: 3 }') response=$(curl --silent --show-error --max-time 30 --request PUT "$API/create.remix_instrumental" \ --header "$AUTH" --header "Content-Type: application/json" --data "$body") result=$(jq -e .remixed_instrumental <<<"$response") || { echo "$response"; exit 1; } id=$(jq -r .id <<<"$result") # 2. Wait. Each call returns as soon as the work is finished, or after 20 seconds. while [ "$(jq -r .status <<<"$result")" = processing ]; do response=$(curl --silent --show-error --max-time 30 "$API/create.remix_instrumental?id=$id&wait=true" \ --header "$AUTH") result=$(jq -e .remixed_instrumental <<<"$response") || { echo "$response"; exit 1; } done # 3. Download each variation, or show what went wrong. if [ "$(jq -r .status <<<"$result")" = failed ]; then jq .error <<<"$result" exit 1 fi number=0 for url in $(jq -r '.outputs[].url' <<<"$result"); do number=$((number + 1)) curl --silent --show-error --fail --output "variation-$number.flac" "$url" echo "Saved variation-$number.flac" done ``` *** ## The result `run` returns the finished `remixed_instrumental` object, with one entry in `outputs` per variation: ```json theme={null} { "id": "op_01ks2qrw5xm28g4h0j1k3n6p9q", "status": "ready", "created_at": "2026-10-10T09:15:02Z", "idempotency_key": "c9d0e1f2-a3b4-4c5d-8e6f-7a8b9c0d1e2f", "n_variations": 3, "prompt": "Dreamy, melancholic synthwave, mid-tempo", "outputs": [ { "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-10-10T09:31:40Z", "duration": 142.7 }, { "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-10-10T09:31:40Z", "duration": 151.0 }, { "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-10-10T09:31:40Z", "duration": 138.4 } ], "error": null } ``` | Field | What it means | | - | - | | `n_variations` | How many variations you asked for. | | `prompt` | The prompt you sent, or `null` if you didn't send one. The source URL is never echoed. | | `loudness` | Present only if you asked for normalisation (not in this example). | | `outputs[].url` | A signed link to one variation, as a FLAC file (44.1 kHz). It needs no API key. | | `outputs[].expires_at` | When the link stops working. Poll the operation again for a fresh one. | | `outputs[].duration` | The variation's length in seconds. | | `outputs[].loudness` | Present only if you asked for normalisation: `lufs`, `true_peak_dbtp` and `compressed`, measured on that variation. | *** ## Choosing parameters * **With or without a prompt?** Leave `prompt` out for variations that stay close to the source. Add one to move them toward a style or mood. Write it as a short phrase or sentence, such as `"A slow, emotional solo piano piece around 80 BPM"`, not a list of tags. The output is always instrumental, so directions about vocals or lyrics have no effect. * **Source.** Use music without vocals. For a song with vocals, use [`create.remix_song`](/guides/create/remix_song). * **`n_variations`.** Ask for two or three when you want to choose; each costs the same as one. * **Loudness.** Off unless you include the block. See the [loudness normalisation guide](/guides/adapt/normalise_loudness#choosing-a-target-by-destination) for which target to pick. *** ## Errors **When you submit** (the request is refused, nothing is charged): | Status | `type` | Why | | - | - | - | | `422` | `string_too_short` / `string_too_long` | `prompt` is empty or over 1,024 characters. (Leave it out rather than sending an empty one.) | | `422` | `missing` | `source` is missing. | | `422` | `source_too_large` | The source's server says the file is over 50 MB. (When it doesn't say, the size is checked during processing instead.) | | `422` | `idempotency_key_reused` | The key was already used for a different endpoint. | | `402` | `insufficient_credits` | Your balance is below 3 credits per variation. | **While processing** (the operation finishes as `failed`, with an `error.code`): | Code | Why | | - | - | | `tos_violation` | The prompt or source was rejected by our content policy. | | `source_unreachable` | The source URL couldn't be fetched. | | `source_format_unsupported` | The file isn't WAV, MP3 or FLAC audio. | | `source_too_short` | The audio is shorter than 1 second. | | `source_too_long` | The audio is longer than 5 minutes. | | `source_too_large` | The file is larger than 50 MB. | | `internal_error`, `generation_timeout` | Something went wrong on our side. Resubmit with the same idempotency key. | **Source limits for this endpoint:** WAV, MP3 or FLAC, from 1 second to 5 minutes long, and at most 50 MB. See [Errors](/errors) for the response shapes, and for the errors every endpoint can return (`401`, `403`, `429`). *** ## Cost **3 credits per variation**, so `n_variations: 3` costs 9. Normalisation costs nothing extra. Resending a request with the same idempotency key is not charged again. # Remix a song Source: https://docs.soundpiece.co/guides/create/remix_song Make new versions of a song with vocals, with new lyrics and a new style `create.remix_song` takes a song that has vocals and makes new versions of it: the vocal line of the original, sung with your new lyrics, in the style your prompt describes. Use it to personalise a song (a name, a brand, an occasion), to re-style a track in another genre, or to try out new words over a melody you already have. The source must contain vocals. To remix music without vocals, use [`create.remix_instrumental`](/guides/create/remix_instrumental). *** ## The request | Field | Required | What it is | | - | - | - | | `source` | yes | The song to remix: `{"url": "https://..."}`, a public link to a WAV, MP3 or FLAC file with vocals. | | `lyrics` | yes | The new words to sing, up to 3,000 characters, with section markers such as `[Verse]` and `[Chorus]`. | | `prompt` | yes | A description of the new style, up to 1,024 characters, e.g. `"acoustic folk, warm, female vocal"`. | | `n_variations` | no | How many different versions to make, from 1 to 3. Defaults to 1. | | `loudness` | no | Normalise every version to a target loudness, at no extra cost. Leave it out to get them as produced. See [Loudness](/loudness). | | `idempotency_key` | yes | A UUID that identifies this job. The [reference client](/client) fills it in for you. | | `callback_url` | no | An HTTPS address to receive a [webhook](/webhooks) when the work is finished. | Every field, with its exact rules: [submit](/api-reference/create/remix_song/submit) and [poll](/api-reference/create/remix_song/poll) in the API Reference. *** ## Example This remixes a song into two acoustic versions with new lyrics and saves them. It uses the [reference client](/client); save `soundpiece.py` or `soundpiece.mjs` next to it first. ```python remix.py theme={null} import soundpiece remix = soundpiece.run("create.remix_song", { "source": {"url": "https://example.com/audio/song.mp3"}, "lyrics": "[Verse]\nMorning light on the harbour wall\n\n[Chorus]\nHappy birthday, Sam, this song is for you", "prompt": "acoustic folk, warm, female vocal", "n_variations": 2, }) for number, output in enumerate(remix["outputs"], start=1): soundpiece.download(output["url"], f"remix-{number}.flac") print(f"Saved remix-{number}.flac") ``` ```javascript remix.mjs theme={null} import { run, download } from "./soundpiece.mjs"; const remix = await run("create.remix_song", { source: { url: "https://example.com/audio/song.mp3" }, lyrics: "[Verse]\nMorning light on the harbour wall\n\n[Chorus]\nHappy birthday, Sam, this song is for you", prompt: "acoustic folk, warm, female vocal", n_variations: 2, }); for (const [index, output] of remix.outputs.entries()) { await download(output.url, `remix-${index + 1}.flac`); console.log(`Saved remix-${index + 1}.flac`); } ``` ```bash cURL theme={null} #!/usr/bin/env bash # Submit, wait and download by hand. Needs curl, jq and uuidgen. set -euo pipefail API=https://api.soundpiece.co/v1 AUTH="Authorization: Bearer $SOUNDPIECE_API_KEY" # 1. Submit. Sending this exact body again (same idempotency_key) is safe. body=$(jq -n --arg key "$(uuidgen)" '{ idempotency_key: $key, source: { url: "https://example.com/audio/song.mp3" }, lyrics: "[Verse]\nMorning light on the harbour wall\n\n[Chorus]\nHappy birthday, Sam, this song is for you", prompt: "acoustic folk, warm, female vocal", n_variations: 2 }') response=$(curl --silent --show-error --max-time 30 --request PUT "$API/create.remix_song" \ --header "$AUTH" --header "Content-Type: application/json" --data "$body") result=$(jq -e .remixed_song <<<"$response") || { echo "$response"; exit 1; } id=$(jq -r .id <<<"$result") # 2. Wait. Each call returns as soon as the work is finished, or after 20 seconds. while [ "$(jq -r .status <<<"$result")" = processing ]; do response=$(curl --silent --show-error --max-time 30 "$API/create.remix_song?id=$id&wait=true" \ --header "$AUTH") result=$(jq -e .remixed_song <<<"$response") || { echo "$response"; exit 1; } done # 3. Download each version, or show what went wrong. if [ "$(jq -r .status <<<"$result")" = failed ]; then jq .error <<<"$result" exit 1 fi number=0 for url in $(jq -r '.outputs[].url' <<<"$result"); do number=$((number + 1)) curl --silent --show-error --fail --output "remix-$number.flac" "$url" echo "Saved remix-$number.flac" done ``` *** ## The result `run` returns the finished `remixed_song` object, with one entry in `outputs` per variation: ```json theme={null} { "id": "op_01ks2qmt3wk17f3g9h0j2m5n8p", "status": "ready", "created_at": "2026-10-10T09:15:02Z", "idempotency_key": "b8c9d0e1-f2a3-4b4c-9d5e-6f7a8b9c0d1e", "lyrics": "[Verse]\nMorning light on the harbour wall\n\n[Chorus]\nHappy birthday, Sam, this song is for you", "prompt": "acoustic folk, warm, female vocal", "n_variations": 2, "outputs": [ { "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-10-10T09:31:40Z", "duration": 176.2 }, { "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-10-10T09:31:40Z", "duration": 181.5 } ], "error": null } ``` | Field | What it means | | - | - | | `lyrics`, `prompt`, `n_variations` | What you asked for. The source URL is never echoed. | | `loudness` | Present only if you asked for normalisation (not in this example). | | `outputs[].url` | A signed link to one version, as a FLAC file (44.1 kHz). It needs no API key. | | `outputs[].expires_at` | When the link stops working. Poll the operation again for a fresh one. | | `outputs[].duration` | The version's length in seconds. | | `outputs[].loudness` | Present only if you asked for normalisation: `lufs`, `true_peak_dbtp` and `compressed`, measured on that version. | *** ## Choosing parameters * **Source.** Use a song with a clear lead vocal; that vocal line is what the new lyrics are sung to. It must be at least 10 seconds long. * **Lyrics.** New lyrics fit best when they follow the shape of the original: a similar number of lines per section, and similar line lengths. * **Prompt.** Describe the new style: genre, mood, tempo, instruments and voice, separated by commas. * **`n_variations`.** Ask for two or three when you want to choose; each costs the same as one. * **Loudness.** Off unless you include the block. See the [loudness normalisation guide](/guides/adapt/normalise_loudness#choosing-a-target-by-destination) for which target to pick. *** ## Errors **When you submit** (the request is refused, nothing is charged): | Status | `type` | Why | | - | - | - | | `422` | `string_too_short` / `string_too_long` | `lyrics` is empty or over 3,000 characters, or `prompt` is empty or over 1,024. | | `422` | `missing` | `source`, `lyrics` or `prompt` is missing. | | `422` | `source_too_large` | The source's server says the file is over 50 MB. (When it doesn't say, the size is checked during processing instead.) | | `422` | `idempotency_key_reused` | The key was already used for a different endpoint. | | `402` | `insufficient_credits` | Your balance is below 3 credits per variation. | **While processing** (the operation finishes as `failed`, with an `error.code`): | Code | Why | | - | - | | `source_has_no_vocals` | The source is valid audio, but no vocals could be found in it. Use [`create.remix_instrumental`](/guides/create/remix_instrumental) for music without vocals. | | `tos_violation` | The lyrics, prompt or source were rejected by our content policy. | | `source_unreachable` | The source URL couldn't be fetched. | | `source_format_unsupported` | The file isn't WAV, MP3 or FLAC audio. | | `source_too_short` | The audio is shorter than 10 seconds. | | `source_too_long` | The audio is longer than 5 minutes. | | `source_too_large` | The file is larger than 50 MB. | | `internal_error`, `generation_timeout` | Something went wrong on our side. Resubmit with the same idempotency key. | **Source limits for this endpoint:** WAV, MP3 or FLAC, from **10 seconds** to 5 minutes long, and at most 50 MB. See [Errors](/errors) for the response shapes, and for the errors every endpoint can return (`401`, `403`, `429`). *** ## Cost **3 credits per variation**, so `n_variations: 2` costs 6. Normalisation costs nothing extra. Resending a request with the same idempotency key is not charged again. # Introduction Source: https://docs.soundpiece.co/introduction ## Welcome Welcome to the Soundpiece API. The Soundpiece API gives you programmatic access to our audio generation, separation, and adaptation capabilities. Generate songs from lyrics, separate stems from a mix, cut audio to length — anything you can do in the Soundpiece app, you can do over HTTP. ## What we offer Produce full-length tracks from lyrics plus a text prompt or a reference track. Generate instrumentals from a prompt or a reference. Create short samples at a target duration, optionally designed to loop seamlessly. Generate single-shot effects — risers, impacts, transitions, foley. Produce fresh variations of an existing track with new lyrics and prompt. Produce fresh variations of an instrumental, optionally steered by a prompt. Separate a mixed track into vocals, drums, bass, and other stems. Trim an existing track to a target duration at musically natural cut points. Bring a track to a standard loudness for streaming, podcasts or broadcast. Mix a voiceover over music, turning the music down while the voice speaks. ## Who this is for * 💻 Developers building music-driven apps and intelligent audio tools * 🎥 Content creators producing sound for social media * 🎚️ Music producers generating or remixing audio at scale * 🧪 Researchers experimenting with generative audio models * 📚 Music libraries making their music adaptive or looking for tagging solutions * 🏢 Digital agencies creating branded or campaign-specific music ## What kind of API is this? Soundpiece is an **RPC-style API**, similar in shape to Slack's or Stripe's APIs. Each endpoint is a named action like `create.new_song` or `adapt.separate_stems`. There are no resource hierarchies to learn — pick the action you want and call it. Three patterns hold across the whole surface: * **`PUT` to submit, `GET` to poll.** Every operation is asynchronous — you submit work and we return immediately with an `id` and `status: "processing"`. You then call the same endpoint with `GET` (or wait for a [webhook](/webhooks)) until `status` is `ready` or `failed`. See [Async operations](/async-operations). * **`idempotency_key` makes retries safe.** Every `PUT` takes a UUID you choose. Sending the same request twice with the same key returns the original response. The key is echoed back on every response and webhook payload so you can correlate work against your own records. * **Signed URLs for downloads.** When `status` is `ready`, each output carries a time-limited signed URL you can `GET` to download FLAC audio. No separate download endpoint; no extra auth on the URL. ## Get started Head to the [Quickstart](/quickstart) for a complete end-to-end example, or jump to one of the core concepts: * [Authentication](/authentication) — API keys, signing secrets, key rotation. * [Async operations](/async-operations) — the submit-poll-download lifecycle every endpoint follows. * [Webhooks](/webhooks) — push notifications when operations complete. * [Errors](/errors) — HTTP error responses and operation-level failure codes. * [Rate limits](/rate-limits) — `429` handling and how capacity is managed. * [Versioning](/versioning) — what's stable, what changes, how deprecations work. ## Support If you spot a bug, need help with an integration, or have feedback, write to [business@soundpiece.co.uk](mailto:business@soundpiece.co.uk). # IP Ownership & Licensing Source: https://docs.soundpiece.co/ip-licensing Who owns what you create with Soundpiece, and what you can do with it This page provides a plain-language summary of Soundpiece's IP and licensing position. It is not legal advice. For more information, contact [business@soundpiece.co.uk](mailto:business@soundpiece.co.uk). *** ## Outputs you generate **You own the music you create.** Audio outputs generated through the Soundpiece API — whether from a text prompt, reference audio, or automated processing — are assigned to you upon generation. Soundpiece does not retain any ownership interest in outputs produced under your account. You are free to: * Use outputs commercially * Sync outputs to video, advertising, or other media * Distribute outputs via streaming, broadcast, or download * Sub-license outputs to your end clients (e.g. if you are an agency producing music on behalf of a brand) * Modify, remix, or build upon outputs *** ## Royalty-free guarantee All Soundpiece-generated audio is **royalty-free**. There are no per-use fees, sync fees, or PRO (Performing Rights Organisation) obligations arising from Soundpiece's side. This applies globally. There are no geographic restrictions on where you may use the music. If you provide an audio `reference` or `source` URL, you are responsible for ensuring you hold the appropriate rights to that material. Soundpiece-generated outputs derived from third-party material may carry the rights obligations of that source. *** ## What Soundpiece retains Soundpiece retains: * The underlying AI models and generation infrastructure * Anonymised, aggregated usage data (never linked to individual outputs) Soundpiece does **not** retain: * Ownership of generated outputs * The right to use your audio for any purpose (including model training) without explicit consent *** ## Prohibited uses The following uses are not permitted under any plan: * Generating music that impersonates a specific living artist's style with the intent to deceive listeners * Using Soundpiece to produce music for use in content that promotes illegal activity, hate speech, or incitement to violence * Attempting to reverse-engineer or extract the underlying AI models *** ## Frequently asked questions Yes. You can sub-license Soundpiece outputs to end clients without restriction. The royalty-free licence transfers with the file. Copyright law on AI-generated content is evolving. In the UK and EU, current guidance suggests that AI-generated works without sufficient human creative input may not attract copyright protection. We recommend seeking specialist legal advice if copyright registration is important for your use case. Yes. Soundpiece outputs are not registered with ContentID or any rights management system. You can upload and monetise them on YouTube, Spotify, and other platforms without Soundpiece asserting any claim. # Loudness Source: https://docs.soundpiece.co/loudness Normalising audio to a target loudness, and reading back what was achieved ## What loudness normalisation is Two tracks peaking at the same level can sound very different in volume. **Integrated loudness** measures how loud a track sounds over its whole length, the way a listener hears it, in **LUFS** (Loudness Units relative to Full Scale, defined by the EBU R128 / ITU-R BS.1770 standards). Normalising a track adjusts it so its integrated loudness lands on a target you choose, so that everything you deliver plays back at a consistent level. You ask for it with a `loudness` block. Seven endpoints accept one: | Endpoint | What gets normalised | Normalisation is | | - | - | - | | [`create.new_song`](/guides/create/new_song) | every variation | opt-in | | [`create.new_instrumental`](/guides/create/new_instrumental) | every variation | opt-in | | [`create.remix_song`](/guides/create/remix_song) | every variation | opt-in | | [`create.remix_instrumental`](/guides/create/remix_instrumental) | every variation | opt-in | | [`adapt.auto_cut_down`](/guides/adapt/auto_cut_down) | the cut | opt-in | | [`adapt.normalise_loudness`](/guides/adapt/normalise_loudness) | a track you supply | always on | | [`adapt.auto_duck`](/guides/adapt/auto_duck) | the mix | always on | `create.new_sample`, `create.new_fx` and `adapt.separate_stems` don't take a `loudness` block. To normalise their outputs, pass them to `adapt.normalise_loudness`. *** ## Asking for it The block takes the target either as a **number** or as a named **preset**, never both: ```json theme={null} { "target_lufs": -18, "allow_compression": true } ``` ```json theme={null} { "preset": "podcast" } ``` | Field | Default | Meaning | | - | - | - | | `target_lufs` | `-14` | The integrated loudness to land on, between `-30` and `-6`. | | `preset` | none | A named destination standing for a fixed target; see [Presets](#presets). Use it instead of `target_lufs`. | | `allow_compression` | `true` | What to do when the target can't be reached by a change of level alone. See [below](#allow_compression). Works with either form. | `{}` asks for the defaults: -14 LUFS, with compression allowed. A block with both `preset` and `target_lufs` is refused with a `422`. ### Opt-in or always on * **Opt-in** on the five endpoints that produce or cut music. Leave `loudness` out and the outputs come back exactly as produced, with no `loudness` fields in the response at all. Include the block, even `{}`, to normalise them. * **Always on** on `adapt.normalise_loudness`, which exists to normalise, and `adapt.auto_duck`, which delivers a finished mix at a known loudness. There the block is optional: leaving it out means the defaults. Normalisation never costs extra. The opt-in endpoints charge their usual price; the two always-on endpoints charge 1 credit per request, normalisation included. *** ## Presets A preset names where the audio is going. Each stands for a fixed target, and **a preset's value never changes**, so a request that names one means the same thing next year as it does today. | Preset | Target | What it's for | | - | - | - | | `streaming` | -14 LUFS | Music for streaming services and online video, which most of them play back at about this level. The same as the default. | | `podcast` | -16 LUFS | Podcasts and other spoken-word audio, the level podcast platforms recommend. | | `broadcast` | -23 LUFS | Television and radio broadcast, the EBU R128 standard. | For a destination that isn't a preset, send its number as `target_lufs`. The [loudness normalisation guide](/guides/adapt/normalise_loudness#choosing-a-target-by-destination) has a table of common destinations. *** ## The peak ceiling Normalised audio always keeps its **true peak at or below -1 dBTP**. That ceiling is fixed, and isn't a field: it leaves the headroom that lossy encoding and playback need to stay free of clipping, and a higher ceiling would only trade that headroom away. Every preset fits under it. *** ## `allow_compression` Changing the level of a track moves its loudness and its peaks by the same amount. Most audio can reach its target that way without its peaks going over the ceiling, and then the level is the only thing that changes, whatever `allow_compression` says. Very dynamic audio, with loud peaks relative to its average level, can't: turning it up far enough to reach the target would push its peaks over the ceiling. `allow_compression` decides what happens then: | Value | Result | | - | - | | `true` (default) | The dynamics are gently compressed so the track lands close to the target, typically within about 1.5 LU. | | `false` | Only the level changes, stopping at the ceiling. The dynamics are untouched, but the track ends up quieter than the target. | Choose `false` when preserving the dynamics matters more to you than hitting the number. The [loudness normalisation guide](/guides/adapt/normalise_loudness#three-possible-outcomes) shows a worked example of each outcome. *** ## What's measured When normalisation was applied, the response tells you two things. The operation **echoes what you asked for**, with any omitted fields filled in: ```json theme={null} "loudness": { "target_lufs": -16.0, "allow_compression": true, "preset": "podcast" } ``` `target_lufs` is always present, whichever way you asked. `preset` is present only when you named one; it is absent (not `null`) when you sent a number or `{}`. Each `ready` output carries a `loudness` block **measured on the audio you download**: ```json theme={null} "loudness": { "lufs": -16.0, "true_peak_dbtp": -2.1, "compressed": false } ``` | Field | Meaning | | - | - | | `lufs` | Integrated loudness measured on the delivered audio. | | `true_peak_dbtp` | True peak measured on the delivered audio. | | `compressed` | Whether reaching the target meant altering the dynamics. `false` means only the level changed. | These are **measured, not assumed**: when a track can't reach the target (see `allow_compression` above), `lufs` reports where it actually landed, so you can decide for yourself whether the difference matters. On an endpoint with variations, each variation is normalised and measured separately. Output `loudness` is only present once the operation is `ready`. On an opt-in endpoint where you didn't ask for normalisation, there is no `loudness` field at all, neither the echo nor on the outputs: absent, not `null`. # Quickstart Source: https://docs.soundpiece.co/quickstart Generate your first song in a few minutes Every Soundpiece request follows the same three steps: 1. **Submit** the request. You get back an operation `id` and `status: "processing"`. 2. **Wait** until `status` is `ready` or `failed`, by polling the same endpoint (or by receiving a [webhook](/webhooks)). 3. **Download** the audio from the signed URL in the result. This guide generates a song from lyrics and a prompt, using the [reference client](/client) to do the waiting and retrying for you. *** ## Step 1: Get your API key API keys are issued from your account dashboard, under **Developer → API keys**. A key is shown **once, when you create it**, so save it somewhere safe. Keys start with `spk_`; the prefix is part of the key. Put the key in an environment variable, so it never appears in your code: ```bash theme={null} export SOUNDPIECE_API_KEY="spk_..." ``` Anyone with your key can use your account. Never put it in code that runs in a browser or a mobile app. If a key is exposed, revoke it from the dashboard and create a new one. See [Authentication](/authentication). *** ## Step 2: Save the reference client Copy the file for your language from [The reference client](/client) into a new folder: * **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. *** ## Step 3: Generate a song Save this program next to the client and run it. Generating a song usually takes a minute or two. ```python song.py theme={null} import soundpiece song = soundpiece.run("create.new_song", { "lyrics": "[Verse]\nDriving all night with the radio on\n\n[Chorus]\nCity lights, carry me home", "prompt": "synthwave, melancholic, mid-tempo", }) soundpiece.download(song["outputs"][0]["url"], "song.flac") print("Saved song.flac") ``` ```javascript song.mjs theme={null} import { run, download } from "./soundpiece.mjs"; const song = await run("create.new_song", { lyrics: "[Verse]\nDriving all night with the radio on\n\n[Chorus]\nCity lights, carry me home", prompt: "synthwave, melancholic, mid-tempo", }); await download(song.outputs[0].url, "song.flac"); console.log("Saved song.flac"); ``` ```bash cURL theme={null} #!/usr/bin/env bash # The same three steps by hand. Needs curl, jq and uuidgen. set -euo pipefail API=https://api.soundpiece.co/v1 AUTH="Authorization: Bearer $SOUNDPIECE_API_KEY" # 1. Submit. Sending this exact body again (same idempotency_key) is safe: # it returns the same operation instead of starting new work. body=$(jq -n --arg key "$(uuidgen)" '{ idempotency_key: $key, lyrics: "[Verse]\nDriving all night with the radio on\n\n[Chorus]\nCity lights, carry me home", prompt: "synthwave, melancholic, mid-tempo" }') response=$(curl --silent --show-error --max-time 30 --request PUT "$API/create.new_song" \ --header "$AUTH" --header "Content-Type: application/json" --data "$body") song=$(jq -e .song <<<"$response") || { echo "$response"; exit 1; } id=$(jq -r .id <<<"$song") # 2. Wait. Each call returns as soon as the song is finished, or after 20 seconds. while [ "$(jq -r .status <<<"$song")" = processing ]; do response=$(curl --silent --show-error --max-time 30 "$API/create.new_song?id=$id&wait=true" \ --header "$AUTH") song=$(jq -e .song <<<"$response") || { echo "$response"; exit 1; } done # 3. Download, or show what went wrong. if [ "$(jq -r .status <<<"$song")" = failed ]; then jq .error <<<"$song" exit 1 fi curl --silent --show-error --fail --output song.flac "$(jq -r '.outputs[0].url' <<<"$song")" echo "Saved song.flac" ``` `run` returns the finished `song` object. It looks like this: ```json theme={null} { "id": "op_01ks2p3hc0etfb59tdwh9edd1r", "status": "ready", "created_at": "2026-05-20T12:34:56Z", "idempotency_key": "550e8400-e29b-41d4-a716-446655440000", "lyrics": "[Verse]\nDriving all night with the radio on\n\n[Chorus]\nCity lights, carry me home", "prompt": "synthwave, melancholic, mid-tempo", "n_variations": 1, "outputs": [ { "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-05-20T12:50:21Z", "duration": 87.4 } ], "error": null } ``` * **`id`** identifies the operation. You can poll it again later with `GET /v1/create.new_song?id=...`. * **`idempotency_key`** is the key the client sent with your request. Sending the same key again returns this same operation instead of starting new work; see [keeping a key across re-runs](/client#keeping-an-idempotency-key-across-re-runs). * **`outputs`** has one entry per variation. Set `n_variations` (1 to 3) in the request for more. * **`url`** is a signed download link for a FLAC file (44.1 kHz). It needs no API key, and stops working at `expires_at`. Poll the operation again for a fresh one. If the song can't be made, `run` raises a `SoundpieceError` carrying the result's `error.code` and `error.message`; see [Errors](/errors). *** ## Next steps The same pattern works for every endpoint. Swap the endpoint name and the request body: | Endpoint | What it does | | - | - | | [`create.new_song`](/guides/create/new_song) | Generate a song from lyrics plus a prompt or a reference track. | | [`create.new_instrumental`](/guides/create/new_instrumental) | Generate an instrumental from a prompt or a reference track. | | [`create.new_sample`](/guides/create/new_sample) | Generate a short sample at a target duration, optionally loopable. | | [`create.new_fx`](/guides/create/new_fx) | Generate a one-shot sound effect. | | [`create.remix_song`](/guides/create/remix_song) | Remix a song with vocals into new variations, guided by new lyrics and a prompt. | | [`create.remix_instrumental`](/guides/create/remix_instrumental) | Remix an instrumental into new variations. | | [`adapt.separate_stems`](/guides/adapt/separate_stems) | Separate a track into vocals, drums, bass, other and instrumental. | | [`adapt.auto_cut_down`](/guides/adapt/auto_cut_down) | Trim a track to a target duration at musically natural cut points. | | [`adapt.normalise_loudness`](/guides/adapt/normalise_loudness) | Normalise a track to a target loudness. | | [`adapt.auto_duck`](/guides/adapt/auto_duck) | Mix a voice over music, turning the music down while the voice speaks. | Each endpoint has a guide with a complete example, its result, its errors and its cost; follow the links in the table. The exact fields are in the API Reference, e.g. [`create.new_song` (submit)](/api-reference/create/new_song/submit). Before you go to production, work through the [production checklist](/best-practices). # Rate limits Source: https://docs.soundpiece.co/rate-limits What 429 means, how to handle it, and how capacity is managed behind the scenes ## Why a per-account rate limit? Rate limits keep the API responsive for every customer. Without them a single misbehaving client — a tight loop accidentally hammering an endpoint — would degrade latency for everyone. The limit you'll encounter is **per account**: every key on the same account shares the same budget. If you need higher throughput, get in touch via [business@soundpiece.co.uk](mailto:business@soundpiece.co.uk). The only `429` you should plan to handle is this one — the **request rate** limit. We manage processing capacity for you separately, so you won't see `429`s from us when our backend is busy — your operation just stays in `processing` longer until capacity frees up. See [Async operations](/async-operations) for that lifecycle. *** ## The 429 response When you exceed your rate limit you get: ```http theme={null} HTTP/1.1 429 Too Many Requests Retry-After: 17 Content-Type: application/json { "detail": "Rate limit exceeded; retry in 17 seconds" } ``` The `Retry-After` header tells you how many seconds to wait before retrying. Honour it — backing off sooner will just bounce off the limit again. We don't currently return `X-RateLimit-*` headers on successful responses. If your integration needs proactive throttling on your side (rather than reactive on `429`), let us know via [business@soundpiece.co.uk](mailto:business@soundpiece.co.uk). *** ## Handling 429 in code Always read `Retry-After` rather than using a fixed delay. Combine with exponential backoff as a fallback if the header is absent or set to `0`: The [reference client](/client) does this for you: its `send` function retries a `429` (as well as network errors and `5xx`) up to five attempts, waiting `Retry-After` seconds when the header gives a wait, and 1, 2, 4, 8 or 16 seconds when it is missing or `0`. If you write your own client, copy that behaviour; the client's source is short enough to read in a few minutes. Because `PUT` endpoints are idempotent on `idempotency_key`, retrying after a `429` is safe — you'll either get the same operation you tried to create the first time, or a fresh one. You won't double-create work. See [Async operations](/async-operations) for the idempotency contract. *** ## Why no `429` for capacity? Audio generation has a long tail of processing time — second to a minute for typical jobs. If we returned `429` every time our processing tier was busy, you'd have to retry repeatedly until capacity opened up, which is wasteful and fragile. Instead we **queue work internally**. When you `PUT`, we accept the operation immediately and return `status: "processing"`. Behind the scenes we dispatch when capacity is available. Your polling continues to show `processing` until the job lands — no `429`, no manual retry of the submit. This means the only places you should plan for `429` are **submit time** (request rate exceeded) — never during long-running operations. *** ## Designing for sustained throughput If you're driving high volume: * **Use webhooks** instead of polling. Polling a hot operation every second consumes the same per-account rate budget your submits are using. See [webhooks](/webhooks). * **Implement a queue on your side** when submitting in bursts. A token-bucket or leaky-bucket sized to your rate limit keeps the submit channel smooth. * **Use one key per service** so you can revoke independently, but remember they share the account's rate budget. For batch jobs at meaningful scale (thousands of operations a day, or sustained per-minute rates beyond your current limit), reach out to [business@soundpiece.co.uk](mailto:business@soundpiece.co.uk) before you start — we can pre-arrange a higher limit and make sure the backend is sized appropriately. # Versioning Source: https://docs.soundpiece.co/versioning How the API is versioned, what counts as a breaking change, and how deprecations are communicated ## Why version in the URL? Every Soundpiece endpoint lives under a `/v1/` URL prefix: ``` https://api.soundpiece.co/v1/create.new_song ``` This commits us to a contract: * Within `/v1/`, you'll never have to read a migration guide. We only add to the surface — new fields, new optional parameters, new endpoints — never break or remove. * When we eventually need a genuinely breaking change, it'll go on `/v2/` and `/v1/` keeps running side by side for at least **6 months** so you have time to migrate without pressure. * You can pin your integration to a major version simply by hardcoding the prefix. No header dance, no opt-in flags. The cost is that one day there'll be a `/v2/`. The benefit is that until then, `/v1/` is stable in a strong sense. *** ## What counts as a breaking change? These require a new major version (`/v2/`): * Removing an endpoint. * Removing a field from a response, or renaming one. * Removing or renaming a required request field. * Changing the type of a field. * Tightening validation (a value that used to be accepted now returns `422`). * Adding a new required request field. * Changing the meaning of a `status` value or other enum. * Changing the auth mechanism. These are **not** breaking and may happen at any time under the current version: * Adding a new endpoint. * Adding a new optional request field (with a sensible default). * Adding a new field to a response object. * Adding a new enum value (e.g. a new `error.code`). Treat unknown codes as the closest stable category — typically `internal_error`. * Loosening validation (a value that used to be rejected is now accepted). * Bug fixes that bring behaviour in line with documented intent. * Performance changes with no observable behaviour change. Build your client to be tolerant of new enum values and new response fields and you'll never need to touch it for a non-breaking change. *** ## Deprecation process When something does need to move from `/v1/` to `/v2/`: 1. **Announcement.** We email all active API key holders and publish the change on the dashboard at least **6 months** before `/v1/` of that endpoint goes away. 2. **Side-by-side availability.** `/v1/` and `/v2/` of the changed endpoint both work during the deprecation window. You migrate at your own pace. 3. **`Sunset` header.** `/v1/` responses on deprecated endpoints carry the [`Sunset` header](https://www.rfc-editor.org/rfc/rfc8594) (an HTTP-date) so your logs and monitoring can pick it up: ```http theme={null} Sunset: Wed, 21 Oct 2026 07:28:00 GMT ``` 4. **Sunset.** After the announced date, the deprecated endpoint returns `410 Gone` with a `detail` field pointing to the replacement. Emergency changes (e.g. critical security fixes) may need a shorter window. We notify affected accounts directly and provide a migration path. *** ## What this looks like in practice A typical year: * **Several non-breaking changes per month** — new endpoints, new optional fields, new error codes. No action needed from you. * **One or two `/v2/` migrations across the whole API** — announced 6 months ahead, dual-running, with a clear migration guide for each affected endpoint. You shouldn't be surprised by anything we ship. If you ever are, that's a bug — write to [business@soundpiece.co.uk](mailto:business@soundpiece.co.uk) and we'll look at how the communication chain broke down. *** ## Recommendations * **Watch the `Sunset` header** in your logs. A request returning `Sunset` doesn't fail — it just tells you when to migrate by. * **Keep `last_used_at` honest** on every API key. We use it to identify who to email when something is changing, so a key that's still being used by an old integration will get the deprecation notice. * **Test against the latest version regularly.** New optional fields and new endpoints sometimes solve problems you didn't realise you had. # Webhooks Source: https://docs.soundpiece.co/webhooks Get pushed the result the moment an operation completes — no polling required ## Overview Supply a `callback_url` on the original `PUT` and we'll `POST` the final response to that URL when the operation reaches `ready` or `failed`. This eliminates polling latency for production integrations. Webhooks follow the [Standard Webhooks](https://www.standardwebhooks.com/) specification, so any standards-compliant client library can verify them out of the box. **Polling is canonical.** Webhooks are a best-effort latency optimisation; if you didn't receive one within a reasonable window, fall back to polling the `GET` endpoint. Don't rely on webhook receipt as proof of operation state. *** ## Registering a callback **An active signing secret is required.** Every webhook is signed, so we only deliver callbacks for accounts that have at least one active [signing secret](/authentication#creating-a-webhook-signing-secret). With no secret there is nothing to sign with, and **no callbacks are sent at all** — even if you supply a `callback_url`. Create one in the dashboard before relying on webhooks. Add `callback_url` to any `PUT` request body: ```json theme={null} { "idempotency_key": "", "lyrics": "...", "prompt": "...", "callback_url": "https://your-app.example.com/webhooks/soundpiece" } ``` Your endpoint must: * Be publicly accessible over **HTTPS** with a valid TLS certificate (self-signed certs are rejected). * Return a **2xx** status code within **10 seconds**. Respond first and do the work afterwards (downloading audio inside the handler can take longer than that). * Handle **duplicate deliveries** idempotently — use the `webhook-id` header as your deduplication key. We re-verify the URL against the same rules at delivery time, so DNS rebinding to a private IP between submission and delivery is rejected too. *** ## Payload shape The body is **exactly what the matching `GET` returns**: the same object, under the same key. There is no extra wrapper. A finished `adapt.auto_duck` request delivers: ```json theme={null} { "ducked_audio": { "id": "op_01ks2pthpgf4qs4fr84g98pbsk", "status": "ready", "created_at": "2026-05-20T12:34:56Z", "idempotency_key": "550e8400-e29b-41d4-a716-446655440000", "loudness": { "target_lufs": -14.0, "allow_compression": true }, "output": { "url": "https://cdn.soundpiece.co/...signed...", "expires_at": "2026-05-20T12:50:21Z", "duration": 94.2, "loudness": { "lufs": -14.0, "true_peak_dbtp": -1.3, "compressed": false } }, "error": null } } ``` The key is the one the endpoint's own response uses — `song` for `create.new_song`, `auto_cut` for `adapt.auto_cut_down`, `ducked_audio` for `adapt.auto_duck`, and so on (see each endpoint's guide, e.g. [New song](/guides/create/new_song#the-result), or its poll page in the API Reference, e.g. [`create.new_song` (poll)](/api-reference/create/new_song/poll)). Anything you can do with a polled response, you can do with a webhook body. The kind of event is sent in the **`webhook-type` header**, `.` — `.ready` or `.failed` — for fan-out routing on your side: for example `create.new_song.ready` or `adapt.auto_duck.failed`. The header is not covered by the signature (see below); the same information is in the signed body, in its key and its `status`. Signed download URLs in a `ready` body are issued when the delivery is sent, so each is valid for its full lifetime from the moment it reaches you; `expires_at` says until when. Correlate webhook deliveries with your own records using the **echoed `idempotency_key`** — you chose it on the `PUT`, so you already have it on your side. *** ## Signature verification Every delivery includes three headers used for verification (per the [Standard Webhooks](https://www.standardwebhooks.com/) spec), plus `webhook-type`: | Header | Value | | - | - | | `webhook-id` | A stable id for this event. Retries reuse the same id, so consumers dedupe on it. | | `webhook-timestamp` | Unix seconds at first delivery attempt. | | `webhook-signature` | `v1,.."))>`. Multiple signatures are space-separated during secret rotation. | | `webhook-type` | The kind of event, `.` — e.g. `adapt.auto_duck.ready`. Not part of the signed content. | To verify a webhook: 1. Compute `HMAC-SHA256(your_signing_secret, "..")`. 2. Base64-encode it. 3. Compare against the `v1,…` signature in `webhook-signature` using **constant-time comparison**. 4. Reject if the signature doesn't match, **or** if `webhook-timestamp` is more than 5 minutes from the current time. Your webhook signing secret is a separate resource from your API key — you create them independently in the dashboard. The plaintext (`whsec_…`) is shown once on creation; we store only an encrypted copy. See [authentication](/authentication). ### Using the Standard Webhooks library (recommended) There are official libraries that hide the HMAC math. From [github.com/standard-webhooks/standard-webhooks](https://github.com/standard-webhooks/standard-webhooks): Both handlers verify the delivery, answer `200` straight away, skip a `webhook-id` they have already seen, and process the result afterwards. ```python Python theme={null} # pip install standardwebhooks flask import logging import os import threading from flask import Flask, abort, request from standardwebhooks import Webhook log = logging.getLogger(__name__) app = Flask(__name__) wh = Webhook(os.environ["SOUNDPIECE_WEBHOOK_SECRET"]) seen_webhook_ids = set() # in production, keep these in your database @app.post("/webhooks/soundpiece") def handle(): try: payload = wh.verify(request.get_data(), request.headers) except Exception: abort(403) webhook_id = request.headers["webhook-id"] # retries reuse the same id if webhook_id not in seen_webhook_ids: seen_webhook_ids.add(webhook_id) # Process in the background (or hand it to your job queue) so we answer in time. threading.Thread(target=handle_result, args=(payload,)).start() return "", 200 def handle_result(payload): (op,) = payload.values() # the same object you'd poll, e.g. payload["song"] if op["status"] == "ready": outputs = op.get("outputs") or [op["output"]] # a list, or a single output for output in outputs: ... # download from output["url"] promptly; it expires at output["expires_at"] else: log.error("soundpiece operation %s failed: %s", op["id"], op["error"]) ``` ```javascript Node.js theme={null} // npm install standardwebhooks express import express from "express"; import { Webhook } from "standardwebhooks"; const app = express(); app.use(express.raw({ type: "application/json" })); const wh = new Webhook(process.env.SOUNDPIECE_WEBHOOK_SECRET); const seenWebhookIds = new Set(); // in production, keep these in your database app.post("/webhooks/soundpiece", (req, res) => { let payload; try { payload = wh.verify(req.body, req.headers); } catch { return res.status(403).end(); } res.status(200).end(); // answer first, then do the work const webhookId = req.headers["webhook-id"]; // retries reuse the same id if (seenWebhookIds.has(webhookId)) return; seenWebhookIds.add(webhookId); handleResult(payload).catch((error) => console.error("webhook processing failed:", error)); }); async function handleResult(payload) { const [op] = Object.values(payload); // the same object you'd poll, e.g. payload.song if (op.status === "ready") { const outputs = op.outputs ?? [op.output]; // a list, or a single output for (const output of outputs) { // download from output.url promptly; it expires at output.expires_at } } else { console.error(`soundpiece operation ${op.id} failed:`, op.error); } } app.listen(3000); ``` Libraries for Go, Ruby, Kotlin, Rust, and PHP are available from the same repository. ### Manual verification If you'd rather not pull in a library, here's the equivalent in plain Python: ```python Python theme={null} import base64 import hashlib import hmac import os import time from flask import Flask, abort, request app = Flask(__name__) SECRET = os.environ["SOUNDPIECE_WEBHOOK_SECRET"] # whsec_<...> # Strip the whsec_ prefix and base64-decode the key material key = base64.b64decode(SECRET.removeprefix("whsec_") + "==") @app.post("/webhooks/soundpiece") def handle(): webhook_id = request.headers["webhook-id"] webhook_ts = request.headers["webhook-timestamp"] webhook_sig = request.headers["webhook-signature"] body = request.get_data() # 5-minute timestamp window if abs(time.time() - int(webhook_ts)) > 300: abort(403) signed = f"{webhook_id}.{webhook_ts}.{body.decode()}".encode() expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode() # Compare against every signature in the (space-separated) header sigs = [s.split(",", 1)[1] for s in webhook_sig.split() if s.startswith("v1,")] if not any(hmac.compare_digest(expected, sig) for sig in sigs): abort(403) payload = request.get_json() ... # skip a webhook_id you've seen; queue the payload to process after responding return "", 200 ``` Always use a constant-time comparison (`hmac.compare_digest` in Python, `crypto.timingSafeEqual` in Node, equivalent in your language). Direct `==` comparison leaks information about the secret via timing. *** ## Retry behaviour If your endpoint doesn't return a `2xx`, we retry with exponential backoff up to **6 attempts** (total window \~9 hours): | Attempt | Delay from previous | | - | - | | 1st | 5 seconds | | 2nd | 30 seconds | | 3rd | 5 minutes | | 4th | 30 minutes | | 5th | 2 hours | | 6th | 6 hours | | Response | Action | | - | - | | `2xx` | Delivered. We won't call again for this operation. | | `4xx` (except `429`) | Fail immediately — this indicates a configuration issue we can't fix by retrying. | | `5xx`, `429`, network error or timeout | Retry. | After the final retry attempt fails we give up and log the failure on our side. The operation result remains available via the `GET` endpoint indefinitely — **fall back to polling** if you suspect a delivery was missed. *** ## Secret rotation Signing secrets are an account-level resource, independent of your API keys — rotating one doesn't touch the other. To rotate without dropping deliveries: 1. Create a new signing secret in the dashboard. The plaintext is shown once. 2. Roll out the new secret to your webhook handler so it can verify against either old or new. 3. During the cutover, every outgoing webhook is signed with **all** of your non-revoked signing secrets (space-separated, e.g. `v1, v1,`). The Standard Webhooks libraries handle multi-signature verification automatically. 4. Revoke the old secret when you're confident every consumer has migrated. *** ## Local testing Use a tunnelling tool like [ngrok](https://ngrok.com) to expose your local server during development: ```bash theme={null} ngrok http 3000 # → Forwarding https://abc123.ngrok.io → http://localhost:3000 ``` Then pass the ngrok URL as `callback_url` when submitting test operations.