# LyricWinter API and MCP documentation (full text) > LyricWinter turns stories into fully cast, multi-voice audio with distinct character voices, directed performances, and sound effects. Developers and AI agents can drive LyricWinter Studio through a beta REST API and a remote MCP server. Notes for agents and developers: - The LyricWinter REST API is in beta. Base URL: https://lyricwinter.com/api/v1. Authenticate with `Authorization: Bearer lw_...`; users create scoped keys at https://lyricwinter.com/dashboard under Developers. - Read the OpenAPI 3.1 contract at https://lyricwinter.com/api/v1/openapi.json instead of guessing endpoints, fields, or enum values. - The remote MCP server is https://lyricwinter.com/mcp (Streamable HTTP, OAuth 2.1 with PKCE, or a Studio-scoped API key as a bearer token). - Studio work is asynchronous: start a workflow, then poll it until its status is completed, partially_completed, failed, or cancelled. Creating audio consumes the account's balance, so never repeat a start request with a new client_mutation_id unless the user wants another run. - Every docs page has a Markdown version at the same URL plus `.md`. Requests that send `Accept: text/markdown` to a docs URL also receive Markdown. --- Source: https://lyricwinter.com/docs.md --- title: "LyricWinter API and MCP server" description: "Build with LyricWinter Studio from code or from an AI agent. The REST API (beta) and the remote MCP server turn stories into fully cast, multi-voice audio." canonical_url: https://lyricwinter.com/docs markdown_url: https://lyricwinter.com/docs.md last_updated: 2026-09-30 status: beta --- # LyricWinter API and MCP server > Build with LyricWinter Studio from code or from an AI agent. The REST API (beta) and the remote MCP server turn stories into fully cast, multi-voice audio. LyricWinter turns written stories into fully cast audio: every character gets a distinct voice, narration and dialogue are directed, and sound effects are placed automatically. Developers can build on LyricWinter Studio in two ways: - **The LyricWinter REST API (beta)** at `https://lyricwinter.com/api/v1`, for scripts, backends, and apps. - **The LyricWinter MCP server** at `https://lyricwinter.com/mcp`, for AI agents such as Claude, ChatGPT, Cursor, VS Code, and Codex. Both use the same Studio account, projects, and balance. Anything you create through one is visible in the other and in the [Studio web app](/studio). > [!NOTE] > The LyricWinter API and MCP server are in beta. They are ready to build on, but fields may be added and some behavior may change before general availability. Check these docs for the current contract. ## What can you build with the LyricWinter API? The LyricWinter API exposes the same Studio engine that powers the web and iOS apps: - **Import stories.** Create a standalone story or a multi-chapter project from plain text. - **Generate multi-voice audio.** One asynchronous workflow detects speakers, casts voices, directs emotion and delivery, places sound effects, and renders audio for up to 16 sections at a time. - **Read and edit the script.** Every section is a versioned document of narration and dialogue blocks that you can read and update without overwriting newer work. - **Play and export.** Stream per-block audio, compile a playback manifest, or export MP3, WAV, M4B, synchronized EPUB, or SRT files. - **Share.** Publish a section or a whole project at a stable public link. - **Bring your own sounds.** Upload sound effects and ambience up to 65 minutes long, let Create Audio place them, and publish them to the public [sound catalog](/docs/api/sounds). ## Should I use the REST API or the MCP server? | If you want to… | Use | | --- | --- | | Automate audio production from your own code or backend | [REST API](/docs/quickstart) | | Let an AI assistant create and manage Studio stories in conversation | [MCP server](/docs/mcp) | | Build a custom app or integration on top of Studio | [REST API](/docs/api) | | Connect Claude, ChatGPT, Cursor, VS Code, or Codex to your account | [MCP server](/docs/mcp) | The MCP server is a thin, authenticated layer over the REST API. It exposes 13 [tools](/docs/mcp-tools) for the most common Studio tasks. The REST API covers more, including exports, sharing, and the sound library. ## How does LyricWinter Studio work? 1. You create a **project** and add **sections** (chapters or scenes) of story text. 2. A **workflow** prepares each section: it splits the text into narration and dialogue blocks, attributes every line to a **speaker**, and casts a voice for each character. 3. The same workflow directs each performance and generates audio. You poll the workflow until it finishes. 4. You play the result block by block, compile a playback manifest, or export a file. Read [Studio concepts](/docs/studio-concepts) for the full model, including versions and safe retries. ## How much does it cost? API and MCP usage draws from the same word balance as the Studio app. Reading data is free. Creating audio consumes words from the account that owns the project. See [pricing](/pricing) for plans and word packs, and check your balance in the [dashboard](/dashboard). ## Machine-readable resources - **OpenAPI 3.1 contract:** [`https://lyricwinter.com/api/v1/openapi.json`](https://lyricwinter.com/api/v1/openapi.json) - **Docs index for LLMs:** [`https://lyricwinter.com/llms.txt`](https://lyricwinter.com/llms.txt), with the full text at [`/llms-full.txt`](https://lyricwinter.com/llms-full.txt) - **Markdown for any page:** add `.md` to the URL, for example [`/docs/quickstart.md`](/docs/quickstart.md) - **MCP OAuth metadata:** [`/.well-known/oauth-protected-resource/mcp`](https://lyricwinter.com/.well-known/oauth-protected-resource/mcp) ## Get help Email [support@lyricwinter.com](mailto:support@lyricwinter.com) and include the `request_id` from any failing response. --- Source: https://lyricwinter.com/docs/quickstart.md --- title: "Quickstart: create Studio audio with the LyricWinter API" description: "Create an API key, add a story, generate multi-voice audio, and fetch playback with the LyricWinter REST API in about ten minutes." canonical_url: https://lyricwinter.com/docs/quickstart markdown_url: https://lyricwinter.com/docs/quickstart.md last_updated: 2026-09-30 status: beta --- # Quickstart: create Studio audio with the LyricWinter API > Create an API key, add a story, generate multi-voice audio, and fetch playback with the LyricWinter REST API in about ten minutes. This quickstart turns a short story into multi-voice audio with the LyricWinter REST API. You will create an API key, add a story, start one Create Audio workflow, wait for it to finish, and fetch playable audio. It takes about ten minutes, most of it waiting for audio. ## Before you begin - A LyricWinter account with words in its balance. Creating audio draws from the same balance as the Studio app. See [pricing](/pricing). - `curl` and a terminal. The last section also shows the same flow in JavaScript and Python. ## Step 1: Create an API key 1. Open your [LyricWinter dashboard](/dashboard) and expand **Developers**. 2. Create a key with the `studio:read`, `studio:write`, and `account:read` scopes. 3. Copy the key. It starts with `lw_` and is shown only once. Export it so the examples below can use it: ```bash export LYRICWINTER_API_KEY="lw_..." ``` Check that the key works: ```bash curl https://lyricwinter.com/api/v1/me \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` Every successful response is wrapped in `{ "data": ..., "request_id": "..." }`. See [Authentication](/docs/authentication) for scopes and key management. ## Step 2: Create a story `POST /studio/sections` creates a standalone Studio story with one section of text. Studio mutations carry two UUIDs: - `actor_session_id` identifies your client session. Generate one per process and reuse it. - `client_mutation_id` identifies this one change. Reuse it only to retry the exact same request. ```bash export ACTOR_SESSION_ID=$(uuidgen | tr 'A-Z' 'a-z') curl -X POST https://lyricwinter.com/api/v1/studio/sections \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schema_version": 1, "actor_session_id": "'"$ACTOR_SESSION_ID"'", "client_mutation_id": "'"$(uuidgen | tr 'A-Z' 'a-z')"'", "title": "The Lighthouse", "raw_text": "The storm had not let up for three days. \"Someone has to light the lamp,\" Mira said. Her brother shook his head. \"Not in this wind.\"" }' ``` The response is `201 Created`. It also includes the section's full `document`. Save `data.project_id` and `data.section.id`: ```json { "data": { "project_id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10", "section": { "id": "c9f0f895-fb98-4b91-9e3f-1d2a6b7c8d20", "project_id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10", "title": "The Lighthouse", "document_version": 1 }, "section_order_version": 1, "speaker_registry_version": 1, "idempotent_replay": false }, "request_id": "req_01J9Z3K8QF4" } ``` > [!TIP] > UUIDs must be lowercase. On macOS, `uuidgen` prints uppercase, so pipe it through `tr 'A-Z' 'a-z'`. ## Step 3: Start Create Audio A `create_audio` workflow does everything in one run: it structures the text into narration and dialogue, attributes lines to speakers, casts a voice for each character, directs emotion and delivery, adds sound effects, and generates the audio. ```bash export PROJECT_ID="" export SECTION_ID="" curl -X POST "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/workflows" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schema_version": 2, "workflow_kind": "create_audio", "actor_session_id": "'"$ACTOR_SESSION_ID"'", "client_mutation_id": "'"$(uuidgen | tr 'A-Z' 'a-z')"'", "section_ids": ["'"$SECTION_ID"'"], "include_generated_sfx": true, "place_project_sounds": true }' ``` The response is `202 Accepted` with the workflow run in `data.run`. Save `data.run.id`. > [!WARNING] > Create Audio consumes words. If a request times out, retry with the **same** `client_mutation_id`. A new ID starts a second, separately billed run. ## Step 4: Wait for the workflow to finish Poll `GET /studio/workflows/{workflowRunId}` every few seconds until `data.run.status` is terminal: | Status | Meaning | | --- | --- | | `queued`, `running`, `canceling` | Still working. Keep polling. | | `completed` | Every step succeeded. | | `partially_completed` | Some steps failed. `data.run.resumable` tells you whether you can resume. | | `failed` | No step succeeded. Check `data.run.error_summary`. | | `cancelled` | The run was cancelled. | ```bash export WORKFLOW_ID="" curl "https://lyricwinter.com/api/v1/studio/workflows/$WORKFLOW_ID" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` `data.run.current_stage` shows what is happening now, such as `casting_voices` or `generating_audio`, and `data.run.completed_step_count` out of `data.run.step_count` gives a progress fraction. ## Step 5: Play the audio `GET /studio/sections/{sectionId}/media` returns one entry per document block, in order. Each block with audio has a short-lived signed `playback_url`: ```bash curl "https://lyricwinter.com/api/v1/studio/sections/$SECTION_ID/media" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ```json { "data": { "section_id": "c9f0f895-fb98-4b91-9e3f-1d2a6b7c8d20", "playback_duration_ms": 14820, "blocks": [ { "block_id": "45c48cce-2e2d-4fbd-8a1c-5e6f7a8b9c30", "freshness": "current", "activity": "idle", "playback_url": "https://storage.googleapis.com/…", "playback_url_expires_at": "2026-09-30T17:15:00.000Z", "content_type": "audio/mpeg" } ] }, "request_id": "req_01J9Z3K8QF4" } ``` Play the blocks in order for the full section, or compile gapless playback with [`GET /studio/sections/{sectionId}/playback-manifest`](/docs/api/media). ## Step 6: Export a file (optional) Exports render the section into one file: `mp3`, `wav`, `m4b`, `epub` (synchronized), or `srt`. An export is pinned to the versions you pass, so read them first: 1. `GET /studio/sections/{sectionId}/document` returns `data.document.document_version` and `data.document.speaker_registry_version`. 2. `GET /studio/projects/{projectId}/mastering-profile` returns `data.mastering_version`. ```bash curl -X POST https://lyricwinter.com/api/v1/studio/exports \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "schema_version": 1, "section_id": "'"$SECTION_ID"'", "document_version": 7, "speaker_registry_version": 3, "mastering_version": 1, "format": "mp3" }' ``` Poll `GET /studio/exports/{exportId}` until `data.status` is `completed`, then download `data.artifact.download_url`. ## Put it together The same flow as one script: ```javascript title="create-audio.mjs" const API = "https://lyricwinter.com/api/v1"; const headers = { Authorization: `Bearer ${process.env.LYRICWINTER_API_KEY}`, "Content-Type": "application/json", }; const actorSessionId = crypto.randomUUID(); async function call(method, path, body) { const response = await fetch(`${API}${path}`, { method, headers, body: body && JSON.stringify(body) }); const payload = await response.json(); if (!response.ok) throw new Error(`${payload.error.code}: ${payload.error.message} (${payload.request_id})`); return payload.data; } const story = await call("POST", "/studio/sections", { schema_version: 1, actor_session_id: actorSessionId, client_mutation_id: crypto.randomUUID(), title: "The Lighthouse", raw_text: 'The storm had not let up for three days. "Someone has to light the lamp," Mira said.', }); const { run } = await call("POST", `/studio/projects/${story.project_id}/workflows`, { schema_version: 2, workflow_kind: "create_audio", actor_session_id: actorSessionId, client_mutation_id: crypto.randomUUID(), section_ids: [story.section.id], }); const terminal = new Set(["completed", "partially_completed", "failed", "cancelled"]); let status = run.status; while (!terminal.has(status)) { await new Promise((resolve) => setTimeout(resolve, 3000)); const workflow = await call("GET", `/studio/workflows/${run.id}`); status = workflow.run.status; console.log(status, workflow.run.current_stage ?? ""); } const media = await call("GET", `/studio/sections/${story.section.id}/media`); console.log(media.blocks.map((block) => block.playback_url).filter(Boolean)); ``` ```python title="create_audio.py" import os, time, uuid import requests API = "https://lyricwinter.com/api/v1" HEADERS = {"Authorization": f"Bearer {os.environ['LYRICWINTER_API_KEY']}"} actor_session_id = str(uuid.uuid4()) def call(method, path, body=None): response = requests.request(method, f"{API}{path}", headers=HEADERS, json=body, timeout=60) payload = response.json() if not response.ok: raise RuntimeError(f"{payload['error']['code']}: {payload['error']['message']} ({payload.get('request_id')})") return payload["data"] story = call("POST", "/studio/sections", { "schema_version": 1, "actor_session_id": actor_session_id, "client_mutation_id": str(uuid.uuid4()), "title": "The Lighthouse", "raw_text": 'The storm had not let up for three days. "Someone has to light the lamp," Mira said.', }) run = call("POST", f"/studio/projects/{story['project_id']}/workflows", { "schema_version": 2, "workflow_kind": "create_audio", "actor_session_id": actor_session_id, "client_mutation_id": str(uuid.uuid4()), "section_ids": [story["section"]["id"]], })["run"] status = run["status"] while status not in {"completed", "partially_completed", "failed", "cancelled"}: time.sleep(3) status = call("GET", f"/studio/workflows/{run['id']}")["run"]["status"] print(status) media = call("GET", f"/studio/sections/{story['section']['id']}/media") print([block["playback_url"] for block in media["blocks"] if block["playback_url"]]) ``` ## Next steps - Learn how projects, sections, documents, and versions fit together in [Studio concepts](/docs/studio-concepts). - Handle conflicts, retries, and errors with [Errors and retries](/docs/errors-and-retries). - Browse every endpoint in the [API reference](/docs/api). - Let an AI agent do all of this for you with the [MCP server](/docs/mcp). --- Source: https://lyricwinter.com/docs/authentication.md --- title: "Authentication and API key scopes" description: "Authenticate LyricWinter API requests with scoped lw_ API keys, choose the right scopes, and understand how MCP clients connect through OAuth 2.1." canonical_url: https://lyricwinter.com/docs/authentication markdown_url: https://lyricwinter.com/docs/authentication.md last_updated: 2026-10-02 status: beta --- # Authentication and API key scopes > Authenticate LyricWinter API requests with scoped lw_ API keys, choose the right scopes, and understand how MCP clients connect through OAuth 2.1. The LyricWinter API authenticates every request with a scoped API key sent as a bearer token. MCP clients can use the same kind of key or sign in through OAuth 2.1, which issues a short-lived Studio key on the user's behalf. ```http Authorization: Bearer lw_... ``` Requests without a valid key return `401 UNAUTHORIZED`. A valid key that lacks the scope an endpoint needs returns `403 FORBIDDEN` with a message such as `API key is missing required scope: studio:write`. ## How do I create a LyricWinter API key? 1. Sign in and open the [dashboard](/dashboard). 2. Expand **Developers** and choose the scopes the key needs. 3. Pick an expiry: never, 7 days, 30 days, 90 days, or 1 year. 4. Create the key and copy it immediately. LyricWinter stores only a hash, so the raw key is shown once. Keys start with `lw_`. Treat them like passwords: keep them in a secret manager or environment variable, never in source control or client-side code. You can also manage keys over the API with `GET /api-keys`, `POST /api-keys`, and `DELETE /api-keys/{apiKeyId}`, which need the `api_keys:read` or `api_keys:write` scope. See the [Account API](/docs/api/account). ## Which scopes does a LyricWinter API key need? Give each key the smallest set of scopes it needs. The scopes used by the endpoints in these docs are: | Scope | Grants | | --- | --- | | `studio:read` | Read Studio projects, sections, documents, speakers, workflows, media, playback, exports, sharing state, and sounds. | | `studio:write` | Create and change Studio content, start and cancel workflows, create exports, publish links, and upload sounds. Includes paid operations. | | `account:read` | Read the account profile with `GET /me`. | | `api_keys:read` | List API keys. | | `api_keys:write` | Create and revoke API keys. | Each endpoint in the [API reference](/docs/api) lists its required scope. The dashboard also offers scopes for other LyricWinter products; they are not needed for Studio. > [!WARNING] > `studio:write` can start Create Audio workflows, which consume words from the account's balance. Give it only to code you trust to spend that balance. ## How do I revoke a key? Revoke a key from the dashboard's **Developers** panel or with `DELETE /api-keys/{apiKeyId}`. Revocation takes effect on the next request. Expired keys stop working automatically. ## How do MCP clients authenticate? The [LyricWinter MCP server](/docs/mcp) accepts two kinds of credentials: - **OAuth 2.1 (recommended).** The client discovers LyricWinter's authorization server from the MCP server's protected-resource metadata, then runs the authorization code flow with PKCE. The user signs in to LyricWinter and approves `studio:read` and, optionally, `studio:write`, `voices:read`, and `voices:write`. LyricWinter issues a one-hour access token and a rotating 30-day refresh token for that client. - **A Studio-scoped API key.** For local development or clients without OAuth support, send a key with `studio:read` and `studio:write` (plus `voices:read` and `voices:write` for the voice tools) as `Authorization: Bearer lw_...`. OAuth access tokens are bound to the MCP server they were issued for. Each person's connection only reaches their own LyricWinter account. To disconnect an MCP client, revoke its key in the dashboard; that also blocks its refresh token. | OAuth metadata | URL | | --- | --- | | Protected resource | `https://lyricwinter.com/.well-known/oauth-protected-resource/mcp` | | Authorization server | `https://lyricwinter.com/.well-known/oauth-authorization-server` | | Authorization endpoint | `https://lyricwinter.com/oauth/authorize` | | Token endpoint | `https://lyricwinter.com/oauth/token` | | Dynamic client registration | `https://lyricwinter.com/oauth/register` | LyricWinter supports public clients (`token_endpoint_auth_method: none`), PKCE with `S256`, the `authorization_code` and `refresh_token` grants, and dynamic client registration with up to five redirect URIs per client. ## Does the API support browser (CORS) requests? No. Call the LyricWinter API from a server, script, or native app, and keep API keys out of browsers. The public [OpenAPI contract](https://lyricwinter.com/api/v1/openapi.json) is the exception: it allows cross-origin reads so that API tools can load it. --- Source: https://lyricwinter.com/docs/studio-concepts.md --- title: "Studio concepts: projects, sections, documents, and workflows" description: "How LyricWinter Studio models stories as projects, sections, versioned documents, speakers, and asynchronous workflows, and how to edit them safely over the API." canonical_url: https://lyricwinter.com/docs/studio-concepts markdown_url: https://lyricwinter.com/docs/studio-concepts.md last_updated: 2026-10-04 status: beta --- # Studio concepts: projects, sections, documents, and workflows > How LyricWinter Studio models stories as projects, sections, versioned documents, speakers, and asynchronous workflows, and how to edit them safely over the API. LyricWinter Studio models a story as a **project** made of ordered **sections**. Each section has a versioned **document** of narration, dialogue, and sound-effect blocks, each spoken line belongs to a project-wide **speaker**, and long-running work such as generating audio happens in asynchronous **workflows**. This page explains each concept and how to change them safely through the LyricWinter API. ## Projects and unassigned stories A **project** is a named container for related sections, such as the chapters of a book. All sections in a project share one cast, so a character keeps the same voice from chapter to chapter. A **standalone story** is a single section that is not yet part of a named project. `POST /studio/sections` creates one. `GET /studio/projects` returns named projects in `projects` and standalone stories in `unassigned_sections`. A standalone story still has a `project_id`, and every project-scoped endpoint works with it. To turn a standalone story into a named project, call `PATCH /studio/projects/{projectId}` with a name. ## Sections A **section** is one chapter, scene, or episode of story text. Sections in a project are ordered. Add one with `POST /studio/projects/{projectId}/sections`, move one with `PATCH /studio/projects/{projectId}/sections/{sectionId}`, and delete one with `DELETE`. Deletions cannot be undone through the API, so call the matching `deletion-info` endpoint first to see what will be removed. ## Documents and blocks Every section has a **document**: the canonical, structured script of the section. Read it with `GET /studio/sections/{sectionId}/document`. A document contains: - `document_version`, which increases on every change. - `speakers`, the project's current speaker registry, and `speaker_registry_version`. - `blocks`, the script in order. Each block has a `kind`: | Block kind | What it is | | --- | --- | | `narration` | Narrated prose, voiced by the narrator. | | `speech` | A line of dialogue attributed to a speaker. | | `sfx` | A sound effect placed in the timeline. | | `note` | A note kept in the script but not voiced. | | `excluded` | Source text kept in the script but intentionally left out of the audio. | | `unparsed` | Text that has not been structured into narration or dialogue yet. It is not voiced until it is. | A new section starts as unstructured text. A `prepare_sections` or `create_audio` workflow turns it into narration and speech blocks. For a finished speaker-labeled script, `POST /studio/sections/{sectionId}/script` creates speech blocks directly, without a prepare workflow or a hand-built AST. Use one `SPEAKER: spoken text` line per turn. A `[direction]` tag at the start of a line steers the whole turn, and a tag mid-line, as in `TORVALD: [softly] Go, Ingvild. [whispers] Please.`, anchors a direction where it appears; both become tracked directions and are removed from the spoken text. A separate `(silence 4s)` or `(silence 4000ms)` line inserts exact silence after the preceding turn. Supply a `speakers` map whose keys exactly match every label and whose values contain `voice_id` and a complete `generation_profile`. For example, `MOM: [quiet, unsteady] I saw his face.\n(silence 4s)\nMOM: I know.` compiles into two speech blocks with a timed-silence point on the first. Direction uses the selected model's supported control, such as an Inworld turn instruction or an ElevenLabs audio tag; models without a suitable whole-turn or mid-line control reject the direction. The route replaces the section's script, preserves its title and section settings, and returns the canonical document. A matching speaker name reuses and recasts that speaker across the project. It makes no generation or provider calls. Unmatched lines, missing speaker mappings, and unsupported directions fail with `400` rather than being dropped. ## Speakers and casting A **speaker** is a character or narrator in a project. The speaker registry is shared by every section in the project and has its own `speaker_registry_version`. Each speaker has a `voice_id` and a generation profile that controls how its lines are performed. `GET /studio/projects/{projectId}/speakers` lists speakers with how often each one appears. Create Audio casts voices automatically; you can recast in the [Studio app](/studio). ### Profiles and provider options Use `PATCH /studio/projects/{projectId}/speakers/{speakerId}/casting` to assign a speaker's voice and complete generation profile together. The request schema types `options` by the provider/model pair. For example: ```json { "provider": "elevenlabs", "model": "eleven_v4", "fallback": "strict", "controls": "best_effort", "options": {} } ``` These are the implemented option keys for connected Studio models. All keys are optional; omit a key to use the provider default. Unsupported keys and invalid values fail before a new profile is saved or paid work starts. | Model | Provider | Accepted `options` | | --- | --- | --- | | `eleven_v4` | `elevenlabs` | `stability` 0-1; integer `seed` 1-4294967295. Empty `{}` preserves defaults. | | `inworld-2` | `inworld` | `apply_text_normalization`: `APPLY_TEXT_NORMALIZATION_UNSPECIFIED`, `ON`, or `OFF`. | | `inworld-1.5-max`, `inworld-1.5-mini` | `inworld` | The normalization option above; `speaking_rate` 0.5-1.5; `temperature` greater than 0 and at most 2. | | `cartesia-sonic-3.6`, `cartesia-sonic-3.5` | `cartesia` | `speed` 0.6-1.5; `volume` 0.5-2. | | `fishaudio`, `fishaudio-s2.1-pro` | `fish_audio` | `temperature` and `top_p` 0-1; `latency`: `low`, `normal`, or `balanced`; integer `chunk_length` 100-300; boolean `normalize` and `condition_on_previous_chunks`. | | `lightning_v3.1` | `smallest_ai` | `{}` only. | | `gpt-4o-mini-tts`, `tts-1-hd`, `tts-1` | `openai` | `{}` only. | | `f5tts` | `f5tts` | `{}` only. | | `stable-audio-3-medium` (generated SFX only) | `stable_audio` | Integer `steps` 1-50. | ElevenLabs seed is best-effort, not a guarantee of deterministic audio or a remedy for repeated words. The connected dialogue transport does not expose `similarity_boost`; v4 does not support `speed`. Unsupported keys are rejected instead of silently ignored. For example, `options: {"stability": 0.5, "seed": 42}` sets only those two supported controls. Use the listed keys for provider options. Delivery directions and expressive controls belong in delivery control sets. A partial block override inherits its missing fields from the speaker's effective profile; its options are checked in that resolved context. Changing an inherited model can make an existing override invalid. Historical document profiles remain readable. Catalog-only profiles such as `inworld-2-experimental`, `chatterbox_fal`, `zyphra`, and `misotts` can still be saved for compatibility, but their Studio generation is not connected. A saved profile does not promise usable audio: check generation availability for the assigned voice and then `/media` for actual audio readiness. ### Preparing a speaker for ElevenLabs v4 Casting and voice preparation are separate. A voice's `default_model` or completed ElevenLabs engine sample does not change a Studio speaker's saved `generation_profile`. There is currently no project-level default generation profile; when no speaker or block override selects a model, Studio uses `inworld-2`. Select `provider: "elevenlabs"` and `model: "eleven_v4"` in the speaker's casting update (or a block override) before checking its readiness. 1. Read `GET /studio/sections/{sectionId}/generation-availability` and find the speech or narration block. Its `generation.requested_profile` identifies the effective model. `generation.status` is `available`, `materialization_required`, or `unavailable`; `generation.options` reports alternatives. 2. If ElevenLabs v4 is `materialization_required`, prepare the assigned voice through `POST /studio/projects/{projectId}/speakers/{speakerId}/voice-materialization-jobs` with `schema_version: 1`, a new `client_mutation_id`, `materialization_kind: "provider_asset"`, `provider: "elevenlabs"`, and `model: "eleven_v4"`. Poll `GET /studio/voice-materialization-jobs/{jobId}` until it completes, then read generation availability again. 3. If you also want to audition the voice, `POST /voices/{voiceId}/engine-samples` with `model: "eleven_v4"` and the voice's current `expected_acoustic_revision` synthesizes the fixed comparison phrase. A successfully completed sample can publish a reusable ElevenLabs provider asset, so it may make Studio ready too. The sample's cached audio is not itself Studio's readiness authority: Studio requires an active provider asset for the voice's current acoustic revision. Always re-read generation availability before generating. For a materializable voice, `generation.reason_code` and the matching option's `reason_code` distinguish `provider_asset_missing` from `acoustic_revision_changed` when a prior-revision provider asset exists. Editing or redesigning a voice can advance its acoustic revision; the old asset then cannot generate new Studio audio. An asset can also be retired or evicted. In either case, prepare the current revision and recheck. Other unavailable conditions use the human-readable `reason` and `diagnostics`; absence of `reason_code` does not mean the block is ready. `POST /ai-voices` currently creates Inworld-designed voices. It has no ElevenLabs Voice Design selector. Preparing such a voice for ElevenLabs v4 changes its synthesis asset, not the design provider or the speaker's selected profile. ## Versions and optimistic concurrency Studio never silently overwrites newer work. Every mutable resource carries a version: | Version | Protects | Where to read it | | --- | --- | --- | | `document_version` | One section's document | `GET /studio/sections/{sectionId}/document` | | `speaker_registry_version` | The project's speakers and casting | The same document response | | `section_order_version` | The order of sections in a project | `GET /studio/projects/{projectId}/sections` | | `mastering_version` | Loudness and mastering settings | `GET /studio/projects/{projectId}/mastering-profile` | When you change something, send the version you last read. If someone else changed it first, the request fails with `409 CONFLICT` instead of overwriting their work. Read the resource again, reapply your change, and retry. Playback compilation and new exports also validate these versions. Versions do not identify audio takes: a retake can replace audio without editing the document or cast. An existing export is an immutable historical file, not a request to compile the newest takes. ### Section pacing Use `set_section_pacing` in `PATCH /studio/sections/{sectionId}/document` to change the silence between consecutive audible passages without replacing the script. Both values are integer milliseconds from `0` through `1000`: ```json { "kind": "set_section_pacing", "same_speaker_gap_ms": 100, "different_speaker_gap_ms": 250 } ``` Wrap the command in the normal document-save request with current `base_document_version`, `base_speaker_registry_version`, session and mutation IDs, and target-version maps. Section-only changes use empty `target_block_versions` and `target_speaker_versions` maps. Read the returned document version before your next edit or export. Existing audio takes remain intact; playback and new exports use the new spacing. ## Mutation IDs and actor sessions Studio write requests include two UUIDs in the JSON body: - **`actor_session_id`** identifies the client session making changes. Generate one when your process starts and reuse it for all requests from that process. - **`client_mutation_id`** identifies one intended change. Generate a new one for each new change, and reuse it only when retrying the exact same request after a timeout or network error. Replaying a `client_mutation_id` with the same body returns the original result. Some creation endpoints return `200` with `idempotent_replay: true` instead of `201`. Reusing it with a different body returns `409`. See [Errors and retries](/docs/errors-and-retries). ## Workflows A **workflow** is a durable, asynchronous job over one or more sections. Start one with `POST /studio/projects/{projectId}/workflows` and poll `GET /studio/workflows/{workflowRunId}`. Creation, polling, cancellation, and resume return `data: { run, steps }`; read the ID and status from `data.run`, not directly from `data`. The latest-project-workflow endpoint is separately nullable when no workflow exists. There are two kinds: | `workflow_kind` | What it does | Cost | Sections per run | | --- | --- | --- | --- | | `prepare_sections` | Structures text into narration and dialogue blocks. Use it to review the script before generating audio. | No audio is generated. | Up to 100 | | `create_audio` | Runs the full pipeline: prepare, assign speakers, cast voices, direct emotion and delivery, place sounds, and generate audio. | Consumes words. | Up to 12 with both sound options on, 14 with one, 16 with none | A Create Audio run moves through these `current_stage` values: `preparing_sections`, `assigning_speakers`, `casting_voices`, `planning_emotion`, `directing_delivery`, `placing_project_sounds`, `directing_sfx`, `preparing_voices`, and `generating_audio`. Stages that are turned off are skipped. A run's `status` is one of `queued`, `running`, `canceling`, `completed`, `partially_completed`, `failed`, or `cancelled`. The last four are terminal. Cancel a run with `POST /studio/workflows/{workflowRunId}/cancel`. Cancellation is cooperative, so work that already started may finish first. When `resumable` is `true` on a failed or partially completed run, `POST /studio/workflows/{workflowRunId}/resume` starts a new linked run that picks up from each section's first failed stage instead of redoing finished work. If the balance runs out, the audio step fails with `error_code` `STUDIO_INSUFFICIENT_BALANCE`; add words, then resume. Workflow progress counts summarize the run. To inspect a particular block's current audio, read `GET /studio/sections/{sectionId}/media` and join by `block_id`. That endpoint exposes freshness, generation activity, errors, and playback URLs. `GET /studio/sections/{sectionId}/generation-availability` answers generation eligibility instead; `available` is not a current-audio status. Create Audio has three independent options in `schema_version` 2: - `include_generated_sfx` adds AI-generated sound effects where the story calls for them. - `place_project_sounds` places sounds from your [sound library](/docs/api/sounds) that are linked to the project. - `direct_emotion` runs the automatic emotion step, which adds delivery directions to spoken blocks that have none for their engine. Set it to `false` to skip the step. All three default to `true`. Direction text sent to the provider is billed, which for ElevenLabs v4 means the audio tags are counted. A run with `direct_emotion` on can therefore cost slightly more than the text alone, and the charge is the text actually sent. ### Checking the balance `GET /me/balance` with `account:read` returns the current settled account balance in `data.balance.snowcoins` and the amount available after holds in `data.balance.available_snowcoins`. This is account funding only. For Studio promotional credits too, call `GET /studio/account-capabilities` with `studio:read` and use `data.studio_funding.total_available_snowcoins` or `total_available_words`. Its `as_of` identifies the snapshot. This endpoint can claim an eligible account reward before reading its balance; repeated reads do not award the same reward again. Subscription state describes the plan, not the remaining generation balance. Capability reads are advisory, not live provider-health checks; the server reserves credits atomically when paid work starts. Billing activity is a historical ledger, not a source of per-event resulting balances. ## Media, playback, and exports - **Generation eligibility.** `GET /studio/sections/{sectionId}/generation-availability` checks the effective model, voice preparation, and compiler constraints. Confirmed ElevenLabs preparation exhaustion makes unprepared ElevenLabs voices unavailable; already prepared current-revision voices remain eligible. ElevenLabs cache additions have a monthly account allowance: Free 3, MyWinter 20, UltraWinter 60. New or renewed preparations count; cached voice reuse and other engines do not. The allowance resets on the first of each month at 00:00 UTC, including annual plans. `generation.status: available` does not mean audio exists or guarantee synthesis quota or funding. `GET /voices/preparation-status` provides a read-only preparation check before selecting or creating a voice; signed-in requests check that account's allowance, and `unknown` means the current allowance could not be verified. - **Media.** `GET /studio/sections/{sectionId}/media` returns one entry per block with its `freshness` (`absent`, `current`, `stale`, or `unsupported`), independent `activity` (`idle`, `generating`, or `failed`), latest generation/import `error`, and a short-lived signed `playback_url` for active audio. Use `freshness: current` plus a non-null playback URL for ready current audio. A failed retake can leave valid current audio available; do not turn `activity: failed` into an assertion that nothing can play. Pinned `project_sound` blocks use an uploaded library revision, not Stable Audio synthesis. They return `generation: null`, as do other blocks to which provider generation does not apply. This is not an assertion that the sound exists or is playable. Read `/media` to check materialization and freshness; do not require every block to have `generation.status: available` before playback or export. ### Boundary pause handling Request `GET /studio/sections/{sectionId}/media?schema_version=2` for each active take's `boundary_silence_handling`. The default response is unchanged. - `timeline`: leading/trailing timed silences are assembled outside the clip. Editing those boundary pauses does not require regenerating its speech. - `fixed_in_asset`: the older generated take includes its authored boundary timing in the audio. Changing those points makes the take stale with `stale_reasons: ["timing"]` and requires regeneration for current export. - `unsupported`: imported or non-speech media has no supported boundary-pause classification. Do not infer editability from its creation timestamp. - `unavailable`: there is no active take to classify. Internal timed silences remain part of the generated asset even in `timeline` mode. Editing an internal pause can still require regeneration. `/media` reports the resulting freshness; a stale historical take can remain playable. ### Playback and export identity - **Playback manifest.** `GET /studio/sections/{sectionId}/playback-manifest` compiles the section's audio, gaps, and sound effects into one timeline for gapless playback. Pass the `document_version` and `speaker_registry_version` you read from the document. - **Exports.** `POST /studio/exports` renders a section into `mp3`, `wav`, `m4b`, synchronized `epub`, or `srt`. Poll `GET /studio/exports/{exportId}` and download `artifact.download_url` when `status` is `completed`. For synchronized playback of an exported file, use `GET /studio/exports/{exportId}/manifest`. Its immutable `manifest.hash` and block timings describe that file, even after a retake or script edit. Do not pair an older file with the current section playback manifest. Block durations include authored silence within the block but not the gaps between blocks. The export manifest's `timing_basis` is `media_and_intra_block_silence_excluding_inter_block_spacing`. The playback manifest's `sequential_units` distinguish `unit_type: "asset"` (the audio asset's own duration), `unit_type: "silence"` (an authored pause assembled on the timeline), and `unit_type: "spacing"` (an inter-block gap). Sound effects appear in `overlay_units`. For speech and narration, an export block's `duration_ms` is the sum of its contiguous asset and silence unit durations; it excludes spacing. Thus an asset unit and its export block can share `start_ms` but have different durations when that block has a trailing pause. Do not replace either duration with the other or normalize the block to its audio asset. Join block-level records by stable `block_id`, and per-unit records by `unit_id`, not by start time or array position. A partial manifest does not contain audio for every document block. `authority.is_current` on the export status is a legacy version comparison: it checks the document, cast, and mastering versions, not audio-take identity. After a retake, create a new export with a new `Idempotency-Key`. Retrying the old key intentionally returns its original export. Newly requested exports compile the current media before content-addressed artifact reuse is checked. Signed URLs expire. Fetch fresh ones from the API instead of storing them. ### Project-wide exports `POST /studio/projects/{projectId}/exports` creates one parent job with a child export for each selected section. Send an `Idempotency-Key` header and: ```json { "schema_version": 1, "format": "mp3", "pin": "current" } ``` The supported formats are `mp3`, `wav`, `m4b`, synchronized `epub`, and `srt`. Omit `sections` to export all active sections in project order, up to 100. For a subset, add `sections` with 1-100 unique section UUIDs belonging to that project; the children still follow project order, not the order of your array. Empty selections, duplicates, and selections over 100 are invalid. For a project with more than 100 active sections, explicitly select a subset of up to 100 per request; omitting `sections` does not truncate the project. The parent selection and version plan are captured together and cannot change. `pin: "current"` captures the saved document, cast, and mastering versions when the request is enqueued. It does **not** capture all audio takes or media revisions atomically at that moment: each child selects and pins its exact media when it compiles against those versions. Save your edits and finish generating the intended audio before requesting an export. This export renders existing media; it does not run TTS or automatically retry failed paid generation. Creation returns `202` for a new job or `200` for a database replay. A retry handled by HTTP idempotency can replay the original `202` and body. Reuse the same key only for the same intended request, then poll for current state; reusing a key with a different body returns `409`. Reordering the same explicit section IDs is the same selection and can replay the original request; changing the selected IDs is a different request. 1. Read the parent ID from `data.id` and poll `GET /studio/projects/{projectId}/exports/{exportId}` while its status is `queued` or `running`. Dispatch is asynchronous; queued children are recovered if needed, but there is no fixed completion deadline. 2. Inspect `data.sections`. Each entry has a `section_id`, captured `title`, child `export_id`, captured `authority` (`document_version`, `speaker_registry_version`, and `mastering_version`), nullable ordinary `export` payload, and nullable `error`. Parent `completed` means every child succeeded; terminal `partial_failed` means some succeeded; terminal `failed` means none succeeded. `percent` averages child progress and counts terminal failures and unavailable children as finished, so 100 percent is not a success guarantee. Check the section error before reading a non-null child's status, error, and artifact. 3. Download each available completed child's `export.artifact.download_url` when its artifact is non-null. For timings matching that file, read `GET /studio/exports/{childExportId}/manifest`; add `schema_version=2` for provider word timing sidecars. Cancel a child with `POST /studio/exports/{childExportId}/cancel`. Use the child ID, not the parent ID, for these operations. There is no combined ZIP or project-level file: `archive` is always null. The parent preserves its captured plan without preventing section transfers or deletion. If a child export moves to another project or is deleted, its section entry keeps the original identity, title, export ID, and authority versions, but returns `export: null` and `error.code: "STUDIO_PROJECT_EXPORT_SECTION_UNAVAILABLE"`. That entry counts as a failure, not a success, in the parent summary. The original child's status, manifest, cancellation, and download access are not guaranteed to remain available after transfer or cleanup. To retrieve earlier parent jobs, use `GET /studio/projects/{projectId}/exports`. Optional `format` filters the list; `cursor` is a nonnegative integer offset, default 0, and `limit` is 1-10, default 1. Pass the returned string `next_cursor` as the next offset, keeping the same filter, until it is null. ### Export version comparisons and selected re-export `GET /studio/projects/{projectId}/export-freshness?format=mp3` returns active sections in project order, excluding deleted sections. Omit `format` to use `mp3`; projects with more than 100 active sections return `400` rather than a truncated freshness list. Each section reports its current `document_version`, `speaker_registry_version`, `mastering_version`, and `latest_export` for that format. With no export, `latest_export` and `matches_current_authority` are both null. The `basis` is `document_cast_mastering_versions`. `matches_current_authority: true` means only that these three saved versions match. It does not prove that the export contains the latest takes, completed successfully, contains all media, or has a valid unexpired artifact. Inspect `latest_export.status` and `latest_export.artifact` separately, and use the child manifest's `completeness` when checking timeline completeness. A retake can leave the version comparison true while the historical file contains older audio. An existing parent never automatically re-exports after edits or retakes. To export updated sections, explicitly create a **new** parent with a new `Idempotency-Key` and a `sections` array containing only the sections you want to render again. For missing or stale audio, repair or regenerate that audio through its normal workflow first; reading freshness or creating a parent is not an automatic paid retry. Keep successful child files from the earlier job and download the new children separately. ### Provider word timings Add `schema_version=2` to the private playback-manifest or export-manifest GET request for `unit_word_timings`. Playback still requires both saved version parameters. The default V1 responses remain unchanged. Each sidecar identifies its `unit_id`, `block_id`, and immutable `asset_id`. `word_timing: "provider"` includes `words` with `text`, `start_ms`, and `end_ms` in section-relative milliseconds, including preceding assembled pauses and gaps. Join by unit ID, not by word or block index. These sidecars do not change the content-addressed audio manifest or its hash. Export reads use only the assets and hashes pinned to that export, never a newer retake's alignment. New Cartesia speech captures normalized spoken-word timestamps in the same synthesis request, with no separate transcription charge. Provider normalization can change word text from the original script; these are not original-text character offsets. Old assets, unsupported providers (including ElevenLabs), SFX, missing alignment, or invalid alignment report `word_timing: "unavailable"` without preventing playback. We do not fabricate timings or transcribe older assets automatically. Alignment is bounded to 10,000 words and 512 KiB per asset. The read's provider timing payload budget is 2 MiB; units beyond it report unavailable. Base manifest and unavailable-unit identity data are separate from that budget. ### Block identity and indexes Join documents, media, workflow issues, and playback using stable `block_id` UUIDs. A `block_index` is zero-based in the full captured document block order, including sound effects and non-audio blocks. It is not a speech-only count. The legacy workflow `block_number` is the corresponding one-based display label. Positions can change after editing; they are not stable identifiers. For indexed failure details, request `GET /studio/workflows/{workflowRunId}/progress?schema_version=2`. It returns the normal ready/total counts plus `block_issues` with `section_id`, `block_id`, `block_index`, and `block_number`. The index records the order when the issue was captured. Use the ID to find the current block after an edit. Omitting the query keeps the original counts-only response for existing clients. Provider text chunks are generation units; never equate their counters with document block positions. ### Sound uploads and long ambience Uploaded project sounds accept files up to **256 MiB** and decoded audio up to **65 minutes**. The completed sound's `current_revision.duration_ms` and `file_size_bytes` describe the verified, sanitized audio; the sanitized WAV may be larger than the original compressed upload, up to **720 MiB**. Upload metadata is not proof of duration: processing rejects audio outside the limits. An uploaded ambience track uses an ordinary SFX block with a pinned `content.source` with `kind: "project_sound"`, `seed: null`, and the source revision's verified duration. To start it with the section, use a placement anchor `{ "kind": "section", "position": "start" }` and `offset_ms: 0`. The overlay plays for the pinned revision's verified duration; it does not loop, trim, or automatically grow to fit later edits. A long bed is not a separate block type. `content.duration_ms` describes the immutable source, not a requested playback length. Changing it on an existing pinned block returns `400 STUDIO_PROJECT_SOUND_DURATION_MISMATCH`, with `expected_duration_ms` and `requested_duration_ms` in the error details. Placement gain and fades remain editable, but a fade does not shorten the section timeline. To use a shorter bed, upload and place a shorter source revision. There is no placement trim or `play_duration_ms` field in this contract. Generated SFX, which omit `content.source`, remain limited to **five minutes**. Do not use the uploaded-sound limit to request longer provider-generated SFX. ## Sharing Publish a section with `POST /studio/sections/{sectionId}/share` or a whole project with `POST /studio/projects/{projectId}/share`. Each returns a stable public link that plays the current audio. `DELETE` on the same path revokes it. See the [Sharing API](/docs/api/sharing). --- Source: https://lyricwinter.com/docs/errors-and-retries.md --- title: "Errors, retries, and idempotency" description: "The LyricWinter API response envelope, error codes, request IDs, retry rules, idempotency keys, and client mutation IDs for safe retries of paid operations." canonical_url: https://lyricwinter.com/docs/errors-and-retries markdown_url: https://lyricwinter.com/docs/errors-and-retries.md last_updated: 2026-10-01 status: beta --- # Errors, retries, and idempotency > The LyricWinter API response envelope, error codes, request IDs, retry rules, idempotency keys, and client mutation IDs for safe retries of paid operations. LyricWinter application API responses use a consistent JSON envelope, and application errors carry a stable `code` and a `retryable` flag. Most paid or state-changing requests support safe retries with an idempotency key or a client mutation ID. Proxy errors and network failures may arrive before the application can produce this envelope. This page explains how to read errors and when it is safe to retry. ## Response envelope Successful responses wrap the result in `data`: ```json { "data": { "id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10" }, "request_id": "req_01J9Z3K8QF4" } ``` List endpoints that paginate add a `pagination` object next to `data`. The public OpenAPI document at `/api/v1/openapi.json` is the one exception: it is returned unwrapped. ## Error format Application errors use the same shape on every endpoint: ```json { "error": { "code": "STUDIO_DOCUMENT_CONFLICT", "message": "Studio changed before this save could commit.", "retryable": false }, "request_id": "req_01J9Z3K8QF4" } ``` - `code` is stable and safe to branch on. `message` is human-readable and may change. - `retryable` is `true` when repeating the same request later can succeed. - `retry_after_ms`, when present, is a nonnegative integer minimum retry delay in milliseconds. Retryable application `429` and `503` responses also send `Retry-After` in whole seconds or as an HTTP-date. A generic five-second hint is minimum backoff, not a prediction of recovery; nonretryable outages do not get a fabricated retry time. - `details` is optional structured context, such as the conflicting version. - The stable `code` is the machine-readable reason. Do not parse human messages or require a duplicate `reason_code` alias. - `request_id` is also sent as the `x-request-id` response header. Include it when you contact support. ## Error codes | HTTP status | `code` | Retryable | What to do | | --- | --- | --- | --- | | 400 | `BAD_REQUEST` | No | Fix the request. `message` names the invalid field. | | 401 | `UNAUTHORIZED` | No | Send a valid, unexpired, unrevoked API key. | | 403 | `FORBIDDEN` | No | The key is missing a required scope, or the resource belongs to another account. | | 404 | `NOT_FOUND` | No | Check the ID. Deleted resources return 404. | | 409 | `CONFLICT` or a Studio conflict code | No | Re-read the resource, then retry with the current version. | | 429 | `RATE_LIMITED` | Yes | Wait for the `Retry-After` header, then retry. | | 500 | `INTERNAL_SERVER_ERROR` | No | Contact support with the `request_id`; do not automatically repeat paid work. | | 502 | `UPSTREAM_UNAVAILABLE` | Yes | A provider is unavailable. Retry with backoff. | | 503 | `SERVICE_UNAVAILABLE` | Yes | Studio is temporarily unavailable. Retry with backoff. | Studio endpoints add more specific codes. Common ones: | `code` | Meaning | | --- | --- | | `STUDIO_DOCUMENT_CONFLICT` | The document changed since the version you sent. | | `STUDIO_SECTION_ORDER_CONFLICT` | The project's section order changed since the version you sent. | | `STUDIO_IDEMPOTENCY_KEY_REUSED` | A `client_mutation_id` was reused with a different request body. | | `STUDIO_GENERATION_PRICE_CHANGED` | The ElevenLabs v4 price changed before generation started. Review the updated quote, then retry. Status 409. | | `STUDIO_GENERATION_PRICE_REJECTED` | One or more generation targets has an invalid price. Refresh the quote before retrying. Status 409. | | `IDEMPOTENCY_KEY_REQUIRED` | The endpoint requires an `Idempotency-Key` header, for example `POST /studio/exports`. | Workflow steps report their own failures in `error_code` and `error_message` on each step. For example, `STUDIO_INSUFFICIENT_BALANCE` means the account ran out of words during generation. ## When is it safe to retry? - **Network errors and timeouts.** The request may or may not have been applied. Retry with the *same* idempotency key or `client_mutation_id`; never generate a new one for a retry. - **`retryable: true`.** Retry with exponential backoff, starting at about one second and capping at about a minute. - **`retry_after_ms` or `Retry-After`.** Wait at least the server's delay, even when it exceeds your usual backoff cap. Only retry automatically when the application also marks the error retryable. - **`409` conflicts.** Do not retry blindly. Re-read the resource, decide whether your change still applies, and send it with the current version and a new mutation ID. - **Other `4xx` errors.** Fix the request first. For a Cloudflare `504` or `524`, a non-JSON gateway response, or a connection failure, do not infer that a start request was rejected. It may have committed. Use the original idempotency key or mutation ID to reconcile the same request, then poll the returned job. Never replace its ID and start duplicate paid work. Provider-account quota failures require us to restore access; a delay hint does not make those failures automatically resumable. ## Idempotency The LyricWinter API uses two idempotency mechanisms. Each endpoint in the [API reference](/docs/api) says which one it uses. **`client_mutation_id` in the body (most Studio writes).** Send a new UUID for each intended change. Replaying the same ID with the same body returns the original result. Some creation endpoints signal a replay with `200` and `idempotent_replay: true` instead of `201`. Reusing an ID with a different body returns `409`. For `POST /studio/sections/{sectionId}/script`, retry an uncertain response with the same body and `client_mutation_id`. The API verifies the original command receipt before accepting a replay, even if the document versions advanced; the response contains the current canonical document, not a historical snapshot. A changed body returns `409`. If the original command can no longer be reconstructed after a capability update, the retry also returns `409` without importing again; read the document to confirm the outcome. This endpoint never starts paid work. **`Idempotency-Key` header (exports and some other endpoints).** Send a unique key of up to 255 characters per logical request. Replaying the same key with the same body returns the original result with the response header `Idempotency-Replayed: true`. Reusing a key with a different body returns `409`. > [!IMPORTANT] > Paid operations such as Create Audio charge once per distinct `client_mutation_id`. Keep the ID with your pending job so a retry after a crash reuses it. ## Polling long-running work Workflows and exports return `202 Accepted` and keep running after the response. Poll the returned resource instead of repeating the start request: - Poll every 2–3 seconds at first, then back off to every 5–10 seconds for long runs. - Stop when the status is terminal. For workflows that is `completed`, `partially_completed`, `failed`, or `cancelled`; for exports it is `completed`, `failed`, `cancelled`, or `superseded`. - Treat signed media URLs as short-lived. Fetch fresh ones when they expire instead of storing them. --- Source: https://lyricwinter.com/docs/api.md --- title: "LyricWinter REST API reference (beta)" description: "Base URL, authentication, response envelope, and every endpoint group in the LyricWinter REST API beta, generated from the public OpenAPI 3.1 contract." canonical_url: https://lyricwinter.com/docs/api markdown_url: https://lyricwinter.com/docs/api.md last_updated: 2026-09-30 status: beta --- # LyricWinter REST API reference (beta) > Base URL, authentication, response envelope, and every endpoint group in the LyricWinter REST API beta, generated from the public OpenAPI 3.1 contract. The LyricWinter REST API is a JSON-over-HTTPS API for LyricWinter Studio. It is in beta. This reference is generated from the public OpenAPI 3.1 contract, the same file that API tools and code generators can download. | Property | Value | | --- | --- | | Base URL | `https://lyricwinter.com/api/v1` | | Authentication | `Authorization: Bearer lw_...` ([details](/docs/authentication)) | | Request format | `Content-Type: application/json`, UTF-8 | | Response format | `{ "data": ..., "request_id": "req_..." }` ([details](/docs/errors-and-retries)) | | IDs | Lowercase UUIDs | | Timestamps | ISO 8601 in UTC | | OpenAPI contract | [`/api/v1/openapi.json`](https://lyricwinter.com/api/v1/openapi.json) | ## Use the OpenAPI contract Load `https://lyricwinter.com/api/v1/openapi.json` into Postman, Insomnia, Bruno, or an OpenAPI code generator to get a typed client for every endpoint below. Coding agents should read the contract instead of guessing field names. ```bash curl https://lyricwinter.com/api/v1/openapi.json -o lyricwinter-openapi.json ``` ## Conventions - **Scopes.** Each endpoint lists the API key scope it requires. Studio endpoints need `studio:read` or `studio:write`. - **Versions.** Writes that change shared state take the version you last read, such as `document_version`, and fail with `409` instead of overwriting newer work. See [Studio concepts](/docs/studio-concepts#versions-and-optimistic-concurrency). - **Safe retries.** Studio writes take a `client_mutation_id`; exports take an `Idempotency-Key` header. See [Errors and retries](/docs/errors-and-retries#idempotency). - **Asynchronous work.** Workflows and exports return `202 Accepted`. Poll the returned resource until it reaches a terminal status. - **Media.** Audio is served from short-lived signed URLs returned by the API. Fetch fresh URLs instead of storing them. ## Endpoint groups ### [Voice preparation](https://lyricwinter.com/docs/api/voice-preparation) Check whether an ElevenLabs voice is already prepared and whether new preparation is currently available. This read-only check does not create voices or reserve provider allowance. - `GET /voices/preparation-status`: [Check voice preparation readiness](https://lyricwinter.com/docs/api/voice-preparation#get-voice-preparation-status) ### [Account and API keys](https://lyricwinter.com/docs/api/account) Check API health, read the account and available generation balance behind an API key, and create, list, or revoke scoped API keys. - `GET /health`: [Check API health](https://lyricwinter.com/docs/api/account#get-health) - `GET /me`: [Get the current account](https://lyricwinter.com/docs/api/account#get-current-account-profile) - `GET /me/generation-preferences`: [Read generation preferences](https://lyricwinter.com/docs/api/account#get-generation-preferences) - `PATCH /me/generation-preferences`: [Save generation preferences](https://lyricwinter.com/docs/api/account#update-generation-preferences) - `GET /me/balance`: [Get the current account balance](https://lyricwinter.com/docs/api/account#get-current-account-balance) - `GET /studio/account-capabilities`: [Get available Studio funding and actions](https://lyricwinter.com/docs/api/account#get-studio-account-capabilities) - `GET /api-keys`: [List API keys](https://lyricwinter.com/docs/api/account#list-api-keys) - `POST /api-keys`: [Create an API key](https://lyricwinter.com/docs/api/account#create-api-key) - `DELETE /api-keys/{apiKeyId}`: [Revoke an API key](https://lyricwinter.com/docs/api/account#revoke-api-key) ### [Projects](https://lyricwinter.com/docs/api/projects) A Studio project groups ordered sections (chapters) that share one cast of speakers. Standalone stories appear as unassigned stories until you turn them into a named project. - `GET /studio/projects`: [List projects and standalone stories](https://lyricwinter.com/docs/api/projects#list-studio-projects) - `PATCH /studio/projects/{projectId}`: [Turn a standalone story into a project](https://lyricwinter.com/docs/api/projects#promote-studio-project) - `GET /studio/projects/{projectId}/deletion-info`: [Preview project deletion](https://lyricwinter.com/docs/api/projects#get-studio-project-deletion-info) - `DELETE /studio/projects/{projectId}`: [Delete a project](https://lyricwinter.com/docs/api/projects#delete-studio-project) ### [Sections](https://lyricwinter.com/docs/api/sections) A section is one chapter or scene of story text inside a project. Create a standalone story with one section, add sections to a project, reorder them, or delete them. - `POST /studio/sections`: [Create a standalone story](https://lyricwinter.com/docs/api/sections#create-standalone-studio-section) - `GET /studio/projects/{projectId}/sections`: [List project sections](https://lyricwinter.com/docs/api/sections#list-studio-project-sections) - `POST /studio/projects/{projectId}/sections`: [Add a section to a project](https://lyricwinter.com/docs/api/sections#create-studio-section) - `PATCH /studio/projects/{projectId}/sections/{sectionId}`: [Move a section](https://lyricwinter.com/docs/api/sections#move-studio-section) - `GET /studio/projects/{projectId}/sections/{sectionId}/deletion-info`: [Preview section deletion](https://lyricwinter.com/docs/api/sections#get-studio-section-deletion-info) - `DELETE /studio/projects/{projectId}/sections/{sectionId}`: [Delete a section](https://lyricwinter.com/docs/api/sections#delete-studio-section) ### [Documents and speakers](https://lyricwinter.com/docs/api/documents) Every section has a versioned document of narration and dialogue blocks attributed to speakers. Read the document before editing it or generating audio, and list the project's speakers and casting. - `GET /studio/sections/{sectionId}/document`: [Get a section document](https://lyricwinter.com/docs/api/documents#get-studio-document) - `PATCH /studio/sections/{sectionId}/document`: [Edit a section document](https://lyricwinter.com/docs/api/documents#save-studio-document) - `POST /studio/sections/{sectionId}/script`: [Import a speaker-labeled script](https://lyricwinter.com/docs/api/documents#import-studio-script) - `GET /studio/projects/{projectId}/speakers`: [List project speakers](https://lyricwinter.com/docs/api/documents#get-studio-project-speaker-usage) - `PATCH /studio/projects/{projectId}/speakers/{speakerId}/casting`: [Assign a speaker's voice and model](https://lyricwinter.com/docs/api/documents#update-studio-project-speaker-casting) ### [Workflows](https://lyricwinter.com/docs/api/workflows) Workflows are durable, asynchronous jobs. A prepare workflow structures text into narration and dialogue; a Create Audio workflow casts voices and generates multi-voice audio. Poll a workflow until it reaches a terminal status. A provider-account limit is reported as a failed step with `STUDIO_WORKFLOW_PROVIDER_QUOTA_EXCEEDED`; generation-batch targets use `PROVIDER_QUOTA_EXCEEDED`. Completed audio remains available. For a shared provider limit, contact us to restore provider access before explicitly resuming. When the failure message says the account's monthly ElevenLabs voice preparation allowance is used up, choose a cached ElevenLabs voice or another engine, or wait for the allowance to reset on the first of the month (UTC). Do not automatically resubmit quota failures. - `POST /studio/projects/{projectId}/workflows`: [Start a workflow](https://lyricwinter.com/docs/api/workflows#create-studio-workflow) - `GET /studio/projects/{projectId}/workflows`: [Get the latest project workflow](https://lyricwinter.com/docs/api/workflows#get-latest-studio-workflow) - `GET /studio/workflows/{workflowRunId}`: [Get a workflow](https://lyricwinter.com/docs/api/workflows#get-studio-workflow) - `GET /studio/workflows/{workflowRunId}/progress`: [Get workflow audio progress](https://lyricwinter.com/docs/api/workflows#get-studio-workflow-progress) - `POST /studio/workflows/{workflowRunId}/cancel`: [Cancel a workflow](https://lyricwinter.com/docs/api/workflows#cancel-studio-workflow) - `POST /studio/workflows/{workflowRunId}/resume`: [Resume a failed workflow](https://lyricwinter.com/docs/api/workflows#resume-studio-workflow) ### [Media and playback](https://lyricwinter.com/docs/api/media) Check whether generation can start separately from whether audio already exists. Read per-block media freshness and generation activity, compile a playback manifest for current audio, and mint short-lived URLs. Generation availability checks voice/compiler eligibility and confirmed ElevenLabs preparation exhaustion; it is not a promise of audio, synthesis quota, or funding. - `GET /studio/sections/{sectionId}/media`: [Get section media](https://lyricwinter.com/docs/api/media#get-studio-section-media) - `GET /studio/sections/{sectionId}/generation-availability`: [Check generation eligibility](https://lyricwinter.com/docs/api/media#get-studio-section-generation-availability) - `GET /studio/sections/{sectionId}/playback-manifest`: [Get a playback manifest](https://lyricwinter.com/docs/api/media#get-studio-playback-manifest) - `POST /studio/media-assets/{assetId}/access-url`: [Create a media access URL](https://lyricwinter.com/docs/api/media#create-studio-media-access-url) ### [Exports](https://lyricwinter.com/docs/api/exports) Render one section or a project's selected sections into downloadable MP3, WAV, M4B, synchronized EPUB, or SRT files. Poll a project job for every child export, then download files individually and fetch each child's immutable manifest for matching timings. No ZIP is produced. Freshness compares document, cast, and mastering versions only, not newer audio takes or artifact readiness. - `GET /studio/projects/{projectId}/mastering-profile`: [Get the mastering profile](https://lyricwinter.com/docs/api/exports#get-studio-mastering-profile) - `POST /studio/projects/{projectId}/exports`: [Export a project's sections](https://lyricwinter.com/docs/api/exports#create-studio-project-export) - `GET /studio/projects/{projectId}/exports`: [List project export jobs](https://lyricwinter.com/docs/api/exports#list-studio-project-exports) - `GET /studio/projects/{projectId}/exports/{exportId}`: [Poll a project export job](https://lyricwinter.com/docs/api/exports#get-studio-project-export) - `GET /studio/projects/{projectId}/export-freshness`: [Compare section export versions](https://lyricwinter.com/docs/api/exports#get-studio-project-export-freshness) - `GET /studio/exports`: [List exports](https://lyricwinter.com/docs/api/exports#list-studio-exports) - `POST /studio/exports`: [Create an export](https://lyricwinter.com/docs/api/exports#create-studio-export) - `GET /studio/exports/{exportId}`: [Get an export](https://lyricwinter.com/docs/api/exports#get-studio-export) - `GET /studio/exports/{exportId}/manifest`: [Get an export's immutable timeline](https://lyricwinter.com/docs/api/exports#get-studio-export-manifest) - `POST /studio/exports/{exportId}/cancel`: [Cancel an export](https://lyricwinter.com/docs/api/exports#cancel-studio-export) ### [Sharing](https://lyricwinter.com/docs/api/sharing) Publish a section or a whole project at a stable public link, read its sharing state, or revoke the link. - `GET /studio/sections/{sectionId}/share`: [Get section sharing](https://lyricwinter.com/docs/api/sharing#get-studio-public-share) - `POST /studio/sections/{sectionId}/share`: [Share a section](https://lyricwinter.com/docs/api/sharing#ensure-studio-public-share) - `DELETE /studio/sections/{sectionId}/share`: [Stop sharing a section](https://lyricwinter.com/docs/api/sharing#revoke-studio-public-share) - `GET /studio/projects/{projectId}/share`: [Get project sharing](https://lyricwinter.com/docs/api/sharing#get-studio-project-share-state) - `POST /studio/projects/{projectId}/share`: [Share a project](https://lyricwinter.com/docs/api/sharing#publish-studio-project) - `DELETE /studio/projects/{projectId}/share`: [Stop sharing a project](https://lyricwinter.com/docs/api/sharing#revoke-studio-project-share) ### [Sound library](https://lyricwinter.com/docs/api/sounds) Upload sound effects and ambience up to 256 MiB and 65 minutes into the Studio sound library, manage their metadata, publish them to the public catalog, and link them to projects so Create Audio can place them automatically. Processing verifies duration; the sanitized WAV can be up to 720 MiB. Pinned project sounds can play as long ambience overlays; provider-generated SFX remain limited to five minutes. - `GET /studio/sounds`: [List sounds](https://lyricwinter.com/docs/api/sounds#list-studio-sounds) - `POST /studio/sounds`: [Start a sound upload](https://lyricwinter.com/docs/api/sounds#create-studio-sound-upload) - `POST /studio/sounds/{soundId}/uploads/{uploadId}/complete`: [Complete a sound upload](https://lyricwinter.com/docs/api/sounds#complete-studio-sound-upload) - `GET /studio/sounds/{soundId}/uploads/latest`: [Get sound upload status](https://lyricwinter.com/docs/api/sounds#get-studio-sound-upload-status) - `GET /studio/sounds/{soundId}`: [Get a sound](https://lyricwinter.com/docs/api/sounds#get-studio-sound) - `PATCH /studio/sounds/{soundId}`: [Update a sound](https://lyricwinter.com/docs/api/sounds#update-studio-sound) - `DELETE /studio/sounds/{soundId}`: [Delete a sound](https://lyricwinter.com/docs/api/sounds#delete-studio-sound) - `POST /studio/sounds/{soundId}/preview-url`: [Create a sound preview URL](https://lyricwinter.com/docs/api/sounds#create-studio-sound-preview-url) - `GET /studio/projects/{projectId}/sounds`: [List project sounds](https://lyricwinter.com/docs/api/sounds#list-studio-project-sounds) - `POST /studio/projects/{projectId}/sounds`: [Link a sound to a project](https://lyricwinter.com/docs/api/sounds#add-studio-project-sound) - `DELETE /studio/projects/{projectId}/sounds/{soundId}`: [Unlink a sound from a project](https://lyricwinter.com/docs/api/sounds#remove-studio-project-sound) - `GET /sounds`: [List public sounds](https://lyricwinter.com/docs/api/sounds#list-public-sounds) - `GET /sounds/{soundId}`: [Get a public sound](https://lyricwinter.com/docs/api/sounds#get-public-sound) ### [Feedback](https://lyricwinter.com/docs/api/feedback) Send product feedback with screenshots and other images so our team can see what you mean. - `POST /feedback`: [Send feedback and images](https://lyricwinter.com/docs/api/feedback#submit-feedback) --- Source: https://lyricwinter.com/docs/api/voice-preparation.md --- title: "Voice preparation API" description: "Check whether an ElevenLabs voice is already prepared and whether new preparation is currently available. This read-only check does not create voices or reserve provider allowance." canonical_url: https://lyricwinter.com/docs/api/voice-preparation markdown_url: https://lyricwinter.com/docs/api/voice-preparation.md last_updated: 2026-10-04 status: beta --- # LyricWinter API reference: Voice preparation > Check whether an ElevenLabs voice is already prepared and whether new preparation is currently available. This read-only check does not create voices or reserve provider allowance. The LyricWinter API is in beta. Base URL: `https://lyricwinter.com/api/v1`. Authenticate every request with `Authorization: Bearer lw_...`. Successful responses are wrapped as `{ "data": ..., "request_id": "req_..." }`. Machine-readable contract: https://lyricwinter.com/api/v1/openapi.json. ## Endpoints - [Check voice preparation readiness](https://lyricwinter.com/docs/api/voice-preparation#get-voice-preparation-status): `GET /voices/preparation-status` ### Check voice preparation readiness `GET /voices/preparation-status` Scope: `voices:read` · Operation ID: `getVoicePreparationStatus` Check current ElevenLabs preparation availability and whether each accessible voice already has a prepared current-revision voice. Signed-in requests also check the requesting account's monthly ElevenLabs allowance: Free 3, MyWinter 20, UltraWinter 60. Each new or renewed ElevenLabs cache addition counts; reuse of a cached voice and other engines do not. Allowances reset on the first of each month at 00:00 UTC, including annual subscriptions. Anonymous requests check provider availability only. Pass up to 50 comma-separated UUIDs in voice_ids, or omit them for a new voice. The response status is available, exhausted, or unknown; the notice distinguishes an account limit from shared provider exhaustion. Already prepared voices can still be used when preparation is exhausted. This check is read-only, does not reserve allowance, and may become stale. Standard Generate may fall back to another engine for an unprepared voice; Studio requires choosing an available engine or waiting for preparation access to return. #### Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `voice_ids` | string | No | Up to 50 comma-separated UUIDs. Omit to check new voice preparation without selecting existing voices. | #### Response 200 Current provider preparation state and cache readiness for accessible voices only. Fields inside `data`: - `provider` ("elevenlabs"; required) - `status` (string, one of `available`, `exhausted`, `unknown`; required) - `notice` (string | null; required) - `voices` (array of object; required) - `voice_id` (string, uuid; required) - `preparation_required` (boolean; required) Errors: `400`, `401`, `403`, `500` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/voices/preparation-status" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` --- Source: https://lyricwinter.com/docs/api/account.md --- title: "Account and API keys API" description: "Check API health, read the account and available generation balance behind an API key, and create, list, or revoke scoped API keys." canonical_url: https://lyricwinter.com/docs/api/account markdown_url: https://lyricwinter.com/docs/api/account.md last_updated: 2026-10-05 status: beta --- # LyricWinter API reference: Account and API keys > Check API health, read the account and available generation balance behind an API key, and create, list, or revoke scoped API keys. The LyricWinter API is in beta. Base URL: `https://lyricwinter.com/api/v1`. Authenticate every request with `Authorization: Bearer lw_...`. Successful responses are wrapped as `{ "data": ..., "request_id": "req_..." }`. Machine-readable contract: https://lyricwinter.com/api/v1/openapi.json. ## Endpoints - [Check API health](https://lyricwinter.com/docs/api/account#get-health): `GET /health` - [Get the current account](https://lyricwinter.com/docs/api/account#get-current-account-profile): `GET /me` - [Read generation preferences](https://lyricwinter.com/docs/api/account#get-generation-preferences): `GET /me/generation-preferences` - [Save generation preferences](https://lyricwinter.com/docs/api/account#update-generation-preferences): `PATCH /me/generation-preferences` - [Get the current account balance](https://lyricwinter.com/docs/api/account#get-current-account-balance): `GET /me/balance` - [Get available Studio funding and actions](https://lyricwinter.com/docs/api/account#get-studio-account-capabilities): `GET /studio/account-capabilities` - [List API keys](https://lyricwinter.com/docs/api/account#list-api-keys): `GET /api-keys` - [Create an API key](https://lyricwinter.com/docs/api/account#create-api-key): `POST /api-keys` - [Revoke an API key](https://lyricwinter.com/docs/api/account#revoke-api-key): `DELETE /api-keys/{apiKeyId}` ### Check API health `GET /health` Authentication: none · Operation ID: `getHealth` Returns a static payload when the LyricWinter API is reachable. It needs no authentication and performs no account checks. #### Response 200 Stable API health response. Fields inside `data`: - `ok` (true; required) - `service` ("lyricwinter"; required) - `version` ("v1"; required) #### Example request ```bash curl "https://lyricwinter.com/api/v1/health" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Get the current account `GET /me` Scope: `account:read` · Operation ID: `getCurrentAccountProfile` Returns the profile of the account that owns the API key: its ID, username, display name, avatar URL, and email. #### Response 200 Current account profile. Fields inside `data`: - `profile` (object; required) - `id` (string; required) - `username` (string | null, 3–30 chars; required) - `name` (string | null, max 100 chars; required) - `image` (string | null, uri; required) - `email` (string | null, email; required) Errors: `401`, `403`, `500` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/me" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Read generation preferences `GET /me/generation-preferences` Scope: `account:read` · Operation ID: `getGenerationPreferences` Read the account's saved Allow fallbacks setting. New accounts start with it enabled. The preference is shared across devices. #### Response 200 Saved generation preference. Fields inside `data`: - `allow_fallbacks` (boolean; required) Errors: `401`, `403`, `500` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/me/generation-preferences" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Save generation preferences `PATCH /me/generation-preferences` Scope: `account:write` · Operation ID: `updateGenerationPreferences` Save Allow fallbacks by sending allow_fallbacks as true or false. The response contains the saved value. Changing this preference does not alter work already submitted. #### Request body (application/json, required) - `allow_fallbacks` (boolean; required) #### Response 200 Saved generation preference. Fields inside `data`: - `allow_fallbacks` (boolean; required) Errors: `400`, `401`, `403`, `500` return the standard error envelope. #### Example request ```bash curl -X PATCH "https://lyricwinter.com/api/v1/me/generation-preferences" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "allow_fallbacks": true }' ``` ### Get the current account balance `GET /me/balance` Scope: `account:read` · Operation ID: `getCurrentAccountBalance` Returns the caller's current account generation balance with `account:read`. `data.balance.snowcoins` is settled; `data.balance.available_snowcoins` is available after outstanding holds. For promotional credits and the combined Studio balance, use `/studio/account-capabilities` with `studio:read`. These values are current snapshots, not historical balances for billing activity. The server still reserves and charges credits atomically. Subscription state and usage history are separate resources. #### Response 200 Cached balance and subscription-adjacent state for the authenticated caller. Fields inside `data`: - `balance` (object; required) - `snowcoins` (integer | null, ≥ 0; required): Current settled account Snowcoins. - `available_snowcoins` (integer | null, ≥ 0; required): Current spendable account Snowcoins after active holds. - `billing_debt` (integer, ≥ 0; required) - `subscription_tier` (string, one of `zerowinter`, `mywinter`, `ultrawinter`, `hyperwinter`; required) - `subscription_provider` (string | null, one of `stripe`, `app_store`; required) - `subscription_status` (string, one of `free`, `active`, `grace_period`, `billing_retry`, `expired`, `revoked`; required) - `has_active_subscription` (boolean; required) - `parse_limit_characters` (integer, ≥ 1; required) - `unlimited_f5_tts` (boolean; required) - `auto_reload_enabled` (boolean; required) - `auto_reload_threshold` (number | null; required) - `small_word_packs_eligible` (boolean; required) - `small_word_pack_eligibility` (object; required) - `canUseSmallWordPacks` (boolean; required) - `activeSubscription` (boolean; required) - `previousSuccessfulPayment` (boolean; required) - `engagedReturningUser` (boolean; required) - `completedAudioRunsLast30Days` (integer, ≥ 0; required) - `accountAgeDays` (integer | null, ≥ 0; required) - `accountCreatedAt` (string | null, date-time; required) Errors: `401`, `403`, `500` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/me/balance" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Get available Studio funding and actions `GET /studio/account-capabilities` Scope: `studio:read` · Operation ID: `getStudioAccountCapabilities` Returns the caller's account balance, promotional Studio credits, plan, and action capabilities with `studio:read`. `data.studio_funding.total_available_snowcoins` and `total_available_words` include both account and promotional funding after holds. `as_of` identifies the snapshot. Actions are advisory, not a live provider-health check or a guarantee that a later paid request will succeed. Reading this endpoint can claim an eligible account reward before returning the snapshot; repeated reads do not grant the same reward twice. #### Response 200 Current Studio account capabilities from one consistent billing snapshot. Fields inside `data`: - `schema_version` ("studio-account-capabilities-4"; required) - `as_of` (string, date-time; required) - `plan` (object; required) - `tier` (string, one of `zerowinter`, `mywinter`, `ultrawinter`, `hyperwinter`; required) - `display_name` (string; required) - `character_limit` (integer, ≥ 1; required) - `balance` (object; required) - `settled_snowcoins` (integer, ≥ 0; required) - `held_snowcoins` (integer, ≥ 0; required) - `available_snowcoins` (integer, ≥ 0; required) - `settled_words` (integer, ≥ 0; required) - `held_words` (integer, ≥ 0; required) - `available_words` (integer, ≥ 0; required) - `studio_funding` (object; required) - `promotional` (object; required) - `available_snowcoins` (integer, ≥ 0; required) - `held_snowcoins` (integer, ≥ 0; required) - `available_words` (integer, ≥ 0; required) - `held_words` (integer, ≥ 0; required) - `grants` (array of object; required) - `campaign_key` (string; required) - `display_name` (string; required) - `available_snowcoins` (integer, ≥ 0; required) - `held_snowcoins` (integer, ≥ 0; required) - `available_words` (integer, ≥ 0; required) - `held_words` (integer, ≥ 0; required) - `expires_at` (string | null, date-time; required) - `total_available_snowcoins` (integer, ≥ 0; required) - `total_held_snowcoins` (integer, ≥ 0; required) - `total_available_words` (integer, ≥ 0; required) - `total_held_words` (integer, ≥ 0; required) - `campaign_dialogs` (array of object, max 20 items; required) - `campaign_key` (string, pattern ^[a-z0-9][a-z0-9_]{2,63}$; required) - `campaign_kind` (string, one of `promotion`, `customer_referral`, `affiliate`, `announcement`, `product_update`; required) - `priority` (integer, -1000–1000; required) - `content_revision` (integer, ≥ 1; required) - `interaction` (string, one of `claim`, `acknowledge`; required) - `title` (string, 1–120 chars; required) - `body` (string, 1–1000 chars; required) - `primary_action_label` (string, 1–80 chars; required) - `secondary_action_label` (string | null, 1–80 chars; required) - `learn_more_url` (string | null, uri, pattern ^https://; required) - `available_until` (string, date-time; required) - `rewards` (array of object, max 20 items; required) - `reward_key` (string; required) - `reward_type` ("studio_words"; required) - `amount_snowcoins` (integer, ≥ 1; required) - `amount_words` (integer, ≥ 1; required) - `expiration_policy` (string, one of `none`, `fixed`, `rolling`; required) - `fixed_expires_at` (string | null, date-time; required) - `expires_in_seconds` (integer | null, ≥ 1; required) - `campaign_offers` (array of object, max 20 items; required) - `campaign_key` (string, pattern ^[a-z0-9][a-z0-9_]{2,63}$; required) - `campaign_kind` (string, one of `promotion`, `customer_referral`, `affiliate`, `announcement`, `product_update`; required) - `priority` (integer, -1000–1000; required) - `content_revision` (integer, ≥ 1; required) - `interaction` (string, one of `claim`, `acknowledge`; required) - `title` (string, 1–120 chars; required) - `body` (string, 1–1000 chars; required) - `primary_action_label` (string, 1–80 chars; required) - `secondary_action_label` (string | null, 1–80 chars; required) - `learn_more_url` (string | null, uri, pattern ^https://; required) - `available_until` (string, date-time; required) - `rewards` (array of object, max 20 items; required) - `reward_key` (string; required) - `reward_type` ("studio_words"; required) - `amount_snowcoins` (integer, ≥ 1; required) - `amount_words` (integer, ≥ 1; required) - `expiration_policy` (string, one of `none`, `fixed`, `rolling`; required) - `fixed_expires_at` (string | null, date-time; required) - `expires_in_seconds` (integer | null, ≥ 1; required) - `purchases` (object; required) - `small_word_packs_eligible` (boolean; required) - `actions` (object; required) - `prepare_text` (object; required) - `status` (string, one of `available`, `not_connected`; required) - `billing_mode` (string, one of `included`, `metered_snowcoins`, `not_applicable`; required) - `speech_generation` (object; required) - `status` (string, one of `available`, `not_connected`; required) - `billing_mode` (string, one of `included`, `metered_snowcoins`, `not_applicable`; required) - `sfx_generation` (object; required) - `status` (string, one of `available`, `not_connected`; required) - `billing_mode` (string, one of `included`, `metered_snowcoins`, `not_applicable`; required) - `voice_design` (object; required) - `status` (string, one of `available`, `not_connected`; required) - `billing_mode` (string, one of `included`, `metered_snowcoins`, `not_applicable`; required) - `voice_cloning` (object; required) - `status` (string, one of `available`, `not_connected`; required) - `billing_mode` (string, one of `included`, `metered_snowcoins`, `not_applicable`; required) - `final_render` (object; required) - `status` (string, one of `available`, `not_connected`; required) - `billing_mode` (string, one of `included`, `metered_snowcoins`, `not_applicable`; required) Errors: `401`, `403`, `500` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/account-capabilities" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### List API keys `GET /api-keys` Scope: `api_keys:read` · Operation ID: `listApiKeys` Lists metadata for the account's API keys, including name, scopes, expiry, last use, and revocation time. Raw keys are never returned. #### Response 200 Caller-owned API key metadata without raw key material or token hashes. Fields inside `data`: - `api_keys` (array of object; required) - `id` (string, uuid; required) - `name` (string, 1–80 chars; required) - `token_prefix` (string, pattern ^lw_[A-Za-z0-9_-]{8,32}$; required): Safe display prefix for identifying a key. - `scopes` (array of string, one of `account:read`, `account:write`, `api_keys:read`, `api_keys:write`, `audio_runs:read`, `audio_runs:write`, `billing:read`, `billing:write`, `demo_videos:read`, `demo_videos:write`, `feedback:write`, `projects:read`, `projects:write`, `reactions:read`, `reactions:write`, `stories:read`, `stories:write`, `studio:read`, `studio:write`, `voice_pins:read`, `voice_pins:write`, `voice_actors:read`, `voice_shares:read`, `voice_shares:write`, `voices:read`, `voices:write`; required) - `expires_at` (string | null, date-time; required) - `last_used_at` (string | null, date-time; required) - `revoked_at` (string | null, date-time; required) - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) Errors: `401`, `403`, `500` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/api-keys" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Create an API key `POST /api-keys` Scope: `api_keys:write` · Operation ID: `createApiKey` Creates a scoped API key. The response contains `raw_key` exactly once; LyricWinter stores only a hash. When you call this endpoint with an API key, the new key can only have scopes that the calling key already holds. #### Request body (application/json, required) - `name` (string, 1–80 chars; required) - `scopes` (array of string, min 1 items, one of `account:read`, `account:write`, `api_keys:read`, `api_keys:write`, `audio_runs:read`, `audio_runs:write`, `billing:read`, `billing:write`, `demo_videos:read`, `demo_videos:write`, `feedback:write`, `projects:read`, `projects:write`, `reactions:read`, `reactions:write`, `stories:read`, `stories:write`, `studio:read`, `studio:write`, `voice_pins:read`, `voice_pins:write`, `voice_actors:read`, `voice_shares:read`, `voice_shares:write`, `voices:read`, `voices:write`; required): Duplicate scopes are accepted and deduplicated. - `expires_at` (string | null, date-time; optional): Optional expiration timestamp. Null or omitted creates a non-expiring key. #### Response 201 API key created. The raw key is returned only in this response. Fields inside `data`: - `api_key` (object; required) - `id` (string, uuid; required) - `name` (string, 1–80 chars; required) - `token_prefix` (string, pattern ^lw_[A-Za-z0-9_-]{8,32}$; required): Safe display prefix for identifying a key. - `scopes` (array of string, one of `account:read`, `account:write`, `api_keys:read`, `api_keys:write`, `audio_runs:read`, `audio_runs:write`, `billing:read`, `billing:write`, `demo_videos:read`, `demo_videos:write`, `feedback:write`, `projects:read`, `projects:write`, `reactions:read`, `reactions:write`, `stories:read`, `stories:write`, `studio:read`, `studio:write`, `voice_pins:read`, `voice_pins:write`, `voice_actors:read`, `voice_shares:read`, `voice_shares:write`, `voices:read`, `voices:write`; required) - `expires_at` (string | null, date-time; required) - `last_used_at` (string | null, date-time; required) - `revoked_at` (string | null, date-time; required) - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) - `raw_key` (string, pattern ^lw_[A-Za-z0-9_-]{8,32}_[A-Za-z0-9_-]+$; required): Display-once raw API key. The server stores only a SHA-256 hash. Errors: `400`, `401`, `403`, `409`, `500` return the standard error envelope. #### Example request ```bash curl -X POST "https://lyricwinter.com/api/v1/api-keys" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "My integration", "scopes": [ "account:read" ] }' ``` ### Revoke an API key `DELETE /api-keys/{apiKeyId}` Scope: `api_keys:write` · Operation ID: `revokeApiKey` Revokes one of the account's API keys immediately. Revoking a key that is already revoked succeeds without changes. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `apiKeyId` | string, uuid | Yes | | #### Response 200 Caller-owned API key was revoked, or was already revoked. Fields inside `data`: - `ok` (true; required) - `api_key` (object; required) - `id` (string, uuid; required) - `name` (string, 1–80 chars; required) - `token_prefix` (string, pattern ^lw_[A-Za-z0-9_-]{8,32}$; required): Safe display prefix for identifying a key. - `scopes` (array of string, one of `account:read`, `account:write`, `api_keys:read`, `api_keys:write`, `audio_runs:read`, `audio_runs:write`, `billing:read`, `billing:write`, `demo_videos:read`, `demo_videos:write`, `feedback:write`, `projects:read`, `projects:write`, `reactions:read`, `reactions:write`, `stories:read`, `stories:write`, `studio:read`, `studio:write`, `voice_pins:read`, `voice_pins:write`, `voice_actors:read`, `voice_shares:read`, `voice_shares:write`, `voices:read`, `voices:write`; required) - `expires_at` (string | null, date-time; required) - `last_used_at` (string | null, date-time; required) - `revoked_at` (string | null, date-time; required) - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) Errors: `400`, `401`, `403`, `404`, `500` return the standard error envelope. #### Example request ```bash curl -X DELETE "https://lyricwinter.com/api/v1/api-keys/$API_KEY_ID" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` --- Source: https://lyricwinter.com/docs/api/projects.md --- title: "Projects API" description: "A Studio project groups ordered sections (chapters) that share one cast of speakers. Standalone stories appear as unassigned stories until you turn them into a named project." canonical_url: https://lyricwinter.com/docs/api/projects markdown_url: https://lyricwinter.com/docs/api/projects.md last_updated: 2026-09-30 status: beta --- # LyricWinter API reference: Projects > A Studio project groups ordered sections (chapters) that share one cast of speakers. Standalone stories appear as unassigned stories until you turn them into a named project. The LyricWinter API is in beta. Base URL: `https://lyricwinter.com/api/v1`. Authenticate every request with `Authorization: Bearer lw_...`. Successful responses are wrapped as `{ "data": ..., "request_id": "req_..." }`. Machine-readable contract: https://lyricwinter.com/api/v1/openapi.json. ## Endpoints - [List projects and standalone stories](https://lyricwinter.com/docs/api/projects#list-studio-projects): `GET /studio/projects` - [Turn a standalone story into a project](https://lyricwinter.com/docs/api/projects#promote-studio-project): `PATCH /studio/projects/{projectId}` - [Preview project deletion](https://lyricwinter.com/docs/api/projects#get-studio-project-deletion-info): `GET /studio/projects/{projectId}/deletion-info` - [Delete a project](https://lyricwinter.com/docs/api/projects#delete-studio-project): `DELETE /studio/projects/{projectId}` ### List projects and standalone stories `GET /studio/projects` Scope: `studio:read` · Operation ID: `listStudioProjects` Returns named projects in `projects` and standalone stories in `unassigned_sections`. Every standalone story still has a `project_id` that works with project-scoped endpoints. #### Response 200 Named projects and unassigned Studio story summaries. Fields inside `data`: - `projects` (array of object; required) - `id` (string, uuid; required) - `name` (string; required) - `project_kind` ("named"; required) - `unassigned_sections` (array of object; required): Standalone Studio stories shown under the virtual Unassigned group, one section per hidden project. - `project_id` (string, uuid; required): Hidden standalone project container that owns the story. - `section` (object; required) - `id` (string, uuid; required) - `project_id` (string, uuid; required) - `title` (string; required) - `document_version` (integer, ≥ 1; required) - `source_text_length` (integer, ≥ 0; required): Length of the section's exact source-text projection in UTF-16 code units. - `activity_status` (string, one of `blank`, `working`, `needs_audio`, `audio_ready`, `audio_failed`; required): Navigator state: working (active workflow or pending audio), audio_failed (latest audio attempt failed), audio_ready (every generatable block has current audio and no unparsed text remains), needs_audio (generatable or unparsed text without complete current audio), or blank (nothing to generate). - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) Errors: `401`, `500` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/projects" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Turn a standalone story into a project `PATCH /studio/projects/{projectId}` Scope: `studio:write` · Operation ID: `promoteStudioProject` Gives a standalone story a project name so you can add more sections to it. Its section, cast, audio, and share links are kept. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | #### Request body (application/json, required) - `schema_version` (1; required) - `name` (string, min 1 chars, pattern \S; required): Project name. Leading and trailing whitespace is trimmed; the trimmed name must be 1-120 characters and must not contain null characters or malformed Unicode. #### Response 200 The promoted named project, or the existing project when it is already named with the same name. Fields inside `data`: - `project` (object; required) - `id` (string, uuid; required) - `name` (string; required) - `project_kind` ("named"; required) Errors: `400`, `401`, `404`, `409`, `500` return the standard error envelope. #### Example request ```bash curl -X PATCH "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schema_version": 1, "name": "My integration" }' ``` ### Preview project deletion `GET /studio/projects/{projectId}/deletion-info` Scope: `studio:read` · Operation ID: `getStudioProjectDeletionInfo` Returns counts of the sections, script blocks, generated audio, and active share links that deleting the project would remove. Call it before deleting. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | #### Response 200 Project deletion preview. Fields inside `data`: - `project_id` (string, uuid; required) - `section_count` (integer, ≥ 0; required) - `other_story_count` (integer, ≥ 0; required) - `block_count` (integer, ≥ 0; required) - `audio_run_count` (integer, ≥ 0; required) - `clip_count` (integer, ≥ 0; required) - `studio_media_count` (integer, ≥ 0; required) - `active_share_link_count` (integer, ≥ 0; required) - `ai_designed_voice_count` (integer, ≥ 0; required) Errors: `401`, `404`, `500` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/deletion-info" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Delete a project `DELETE /studio/projects/{projectId}` Scope: `studio:write` · Operation ID: `deleteStudioProject` Deletes a project with all of its sections, in-progress work, playback, and share links. This cannot be undone through the API. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | #### Request body (application/json, required) - `schema_version` (1; required) - `expected_project_kind` (string, one of `standalone`, `named`; required): The project kind the deleting client most recently observed. The server rejects deletion if the live kind has changed. - `delete_ai_designed_voices` (boolean; required) #### Response 200 Project deletion counts and optional AI-designed voice cleanup result. Fields inside `data`: - `success` (true; required) - `project_id` (string, uuid; required) - `mode` ("project_and_stories"; required) - `stories_deleted` (integer, ≥ 0; required) - `audio_runs_deleted` (integer, ≥ 0; required) - `clips_deleted` (integer, ≥ 0; required) - `public_stories_deleted` (integer, ≥ 0; required) - `shared_stories_deleted` (integer, ≥ 0; required) - `active_share_links_deleted` (integer, ≥ 0; required) - `studio_sections_deleted` (integer, ≥ 0; required) - `studio_media_marked_for_deletion` (integer, ≥ 0; required) - `files_deleted` (integer, ≥ 0; required) - `ai_designed_voices_deleted` (integer, ≥ 0; required) - `ai_designed_voices_retained` (integer, ≥ 0; required) Errors: `400`, `401`, `404`, `409`, `500` return the standard error envelope. #### Example request ```bash curl -X DELETE "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schema_version": 1, "expected_project_kind": "standalone", "delete_ai_designed_voices": true }' ``` --- Source: https://lyricwinter.com/docs/api/sections.md --- title: "Sections API" description: "A section is one chapter or scene of story text inside a project. Create a standalone story with one section, add sections to a project, reorder them, or delete them." canonical_url: https://lyricwinter.com/docs/api/sections markdown_url: https://lyricwinter.com/docs/api/sections.md last_updated: 2026-09-30 status: beta --- # LyricWinter API reference: Sections > A section is one chapter or scene of story text inside a project. Create a standalone story with one section, add sections to a project, reorder them, or delete them. The LyricWinter API is in beta. Base URL: `https://lyricwinter.com/api/v1`. Authenticate every request with `Authorization: Bearer lw_...`. Successful responses are wrapped as `{ "data": ..., "request_id": "req_..." }`. Machine-readable contract: https://lyricwinter.com/api/v1/openapi.json. ## Endpoints - [Create a standalone story](https://lyricwinter.com/docs/api/sections#create-standalone-studio-section): `POST /studio/sections` - [List project sections](https://lyricwinter.com/docs/api/sections#list-studio-project-sections): `GET /studio/projects/{projectId}/sections` - [Add a section to a project](https://lyricwinter.com/docs/api/sections#create-studio-section): `POST /studio/projects/{projectId}/sections` - [Move a section](https://lyricwinter.com/docs/api/sections#move-studio-section): `PATCH /studio/projects/{projectId}/sections/{sectionId}` - [Preview section deletion](https://lyricwinter.com/docs/api/sections#get-studio-section-deletion-info): `GET /studio/projects/{projectId}/sections/{sectionId}/deletion-info` - [Delete a section](https://lyricwinter.com/docs/api/sections#delete-studio-section): `DELETE /studio/projects/{projectId}/sections/{sectionId}` ### Create a standalone story `POST /studio/sections` Scope: `studio:write` · Retries: reuse the same `client_mutation_id` · Operation ID: `createStandaloneStudioSection` Creates a standalone Studio story with one section containing the exact text you send. Returns `201` with the new project ID, section, and document, or `200` with `idempotent_replay: true` when the same `client_mutation_id` was already applied. #### Request body (application/json, required) - `schema_version` (1; required) - `actor_session_id` (string, uuid; required) - `client_mutation_id` (string, uuid; required): Idempotency key; a replay with the same canonical request returns the committed result. - `title` (string, max 500 chars; required): At most 500 UTF-16 code units. Must not contain null characters or malformed Unicode. - `raw_text` (string; required): Exact section text stored as one unparsed block. At most 8 MiB of UTF-8 (the whole request body is also limited to 8 MiB). Must not contain null characters or malformed Unicode. #### Response 200 Idempotent replay of the existing standalone story. Fields inside `data`: - `project_id` (string, uuid; required) - `section` (object; required) - `id` (string, uuid; required) - `project_id` (string, uuid; required) - `title` (string; required) - `document_version` (integer, ≥ 1; required) - `source_text_length` (integer, ≥ 0; required): Length of the section's exact source-text projection in UTF-16 code units. - `activity_status` (string, one of `blank`, `working`, `needs_audio`, `audio_ready`, `audio_failed`; required): Navigator state: working (active workflow or pending audio), audio_failed (latest audio attempt failed), audio_ready (every generatable block has current audio and no unparsed text remains), needs_audio (generatable or unparsed text without complete current audio), or blank (nothing to generate). - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) - `document` (object; required): The canonical structured script of one section (studio-ast-2): its versions, speakers, and ordered blocks. See [Get a section document](https://lyricwinter.com/docs/api/documents#get-studio-document) for its fields. - `section_order_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `idempotent_replay` (boolean; required): True when client_mutation_id matched an already committed creation (HTTP 200); false for a new section (HTTP 201). #### Response 201 Standalone Studio story created. Fields inside `data`: - `project_id` (string, uuid; required) - `section` (object; required) - `id` (string, uuid; required) - `project_id` (string, uuid; required) - `title` (string; required) - `document_version` (integer, ≥ 1; required) - `source_text_length` (integer, ≥ 0; required): Length of the section's exact source-text projection in UTF-16 code units. - `activity_status` (string, one of `blank`, `working`, `needs_audio`, `audio_ready`, `audio_failed`; required): Navigator state: working (active workflow or pending audio), audio_failed (latest audio attempt failed), audio_ready (every generatable block has current audio and no unparsed text remains), needs_audio (generatable or unparsed text without complete current audio), or blank (nothing to generate). - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) - `document` (object; required): The canonical structured script of one section (studio-ast-2): its versions, speakers, and ordered blocks. See [Get a section document](https://lyricwinter.com/docs/api/documents#get-studio-document) for its fields. - `section_order_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `idempotent_replay` (boolean; required): True when client_mutation_id matched an already committed creation (HTTP 200); false for a new section (HTTP 201). Errors: `400`, `401`, `409`, `500` return the standard error envelope. #### Example request ```bash curl -X POST "https://lyricwinter.com/api/v1/studio/sections" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schema_version": 1, "actor_session_id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10", "client_mutation_id": "c9f0f895-fb98-4b91-9e3f-1d2a6b7c8d20", "title": "Chapter One", "raw_text": "A bell rang. \"Who is there?\" Mira asked." }' ``` ### List project sections `GET /studio/projects/{projectId}/sections` Scope: `studio:read` · Operation ID: `listStudioProjectSections` Returns a project's sections in order with the project's `section_order_version` and `speaker_registry_version`. A project that has not been opened in Studio yet returns `initialized: false`. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | #### Response 200 Studio project state and sections. Fields inside `data`: - `project_id` (string, uuid; required) - `initialized` (boolean; required): False when the owned project has no Studio state yet; the nullable fields are then null and sections is empty. - `casting_defaults_mode` (string | null, one of `apply_exact`, `suggest`, `ignore`; required) - `default_automation_profile` (string | null, one of `quick`, `balanced`, `directed`; required) - `section_order_version` (integer | null, ≥ 1; required) - `speaker_registry_version` (integer | null, ≥ 1; required) - `sections` (array of object; required): Active sections in project order. - `id` (string, uuid; required) - `project_id` (string, uuid; required) - `title` (string; required) - `document_version` (integer, ≥ 1; required) - `source_text_length` (integer, ≥ 0; required): Length of the section's exact source-text projection in UTF-16 code units. - `activity_status` (string, one of `blank`, `working`, `needs_audio`, `audio_ready`, `audio_failed`; required): Navigator state: working (active workflow or pending audio), audio_failed (latest audio attempt failed), audio_ready (every generatable block has current audio and no unparsed text remains), needs_audio (generatable or unparsed text without complete current audio), or blank (nothing to generate). - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) Errors: `400`, `401`, `403`, `500` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/sections" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Add a section to a project `POST /studio/projects/{projectId}/sections` Scope: `studio:write` · Retries: reuse the same `client_mutation_id` · Operation ID: `createStudioSection` Adds one section of unstructured text to a project. Standalone projects allow only one active section; adding another returns `400 STUDIO_COMMAND_REJECTED`. Use a named project created through `POST /projects` for multiple sections. Send the `section_order_version` you last read as `base_section_order_version`; a stale version returns `409`. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | #### Request body (application/json, required) - `schema_version` (1; required) - `actor_session_id` (string, uuid; required) - `client_mutation_id` (string, uuid; required): Idempotency key; a replay with the same canonical request returns the committed result. - `base_section_order_version` (integer | null, ≥ 1; required): Section-order version the client last observed; null when the project has not initialized Studio. - `title` (string, max 500 chars; required): At most 500 UTF-16 code units. Must not contain null characters or malformed Unicode. - `raw_text` (string; required): Exact section text stored as one unparsed block. At most 8 MiB of UTF-8 (the whole request body is also limited to 8 MiB). Must not contain null characters or malformed Unicode. #### Response 200 Idempotent replay of the existing section. Fields inside `data`: - `project_id` (string, uuid; required) - `section` (object; required) - `id` (string, uuid; required) - `project_id` (string, uuid; required) - `title` (string; required) - `document_version` (integer, ≥ 1; required) - `source_text_length` (integer, ≥ 0; required): Length of the section's exact source-text projection in UTF-16 code units. - `activity_status` (string, one of `blank`, `working`, `needs_audio`, `audio_ready`, `audio_failed`; required): Navigator state: working (active workflow or pending audio), audio_failed (latest audio attempt failed), audio_ready (every generatable block has current audio and no unparsed text remains), needs_audio (generatable or unparsed text without complete current audio), or blank (nothing to generate). - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) - `document` (object; required): The canonical structured script of one section (studio-ast-2): its versions, speakers, and ordered blocks. See [Get a section document](https://lyricwinter.com/docs/api/documents#get-studio-document) for its fields. - `section_order_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `idempotent_replay` (boolean; required): True when client_mutation_id matched an already committed creation (HTTP 200); false for a new section (HTTP 201). #### Response 201 Studio section created. Fields inside `data`: - `project_id` (string, uuid; required) - `section` (object; required) - `id` (string, uuid; required) - `project_id` (string, uuid; required) - `title` (string; required) - `document_version` (integer, ≥ 1; required) - `source_text_length` (integer, ≥ 0; required): Length of the section's exact source-text projection in UTF-16 code units. - `activity_status` (string, one of `blank`, `working`, `needs_audio`, `audio_ready`, `audio_failed`; required): Navigator state: working (active workflow or pending audio), audio_failed (latest audio attempt failed), audio_ready (every generatable block has current audio and no unparsed text remains), needs_audio (generatable or unparsed text without complete current audio), or blank (nothing to generate). - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) - `document` (object; required): The canonical structured script of one section (studio-ast-2): its versions, speakers, and ordered blocks. See [Get a section document](https://lyricwinter.com/docs/api/documents#get-studio-document) for its fields. - `section_order_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `idempotent_replay` (boolean; required): True when client_mutation_id matched an already committed creation (HTTP 200); false for a new section (HTTP 201). Errors: `400`, `401`, `403`, `409`, `500` return the standard error envelope. #### Example request ```bash curl -X POST "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/sections" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schema_version": 1, "actor_session_id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10", "client_mutation_id": "c9f0f895-fb98-4b91-9e3f-1d2a6b7c8d20", "base_section_order_version": 1, "title": "Chapter One", "raw_text": "A bell rang. \"Who is there?\" Mira asked." }' ``` ### Move a section `PATCH /studio/projects/{projectId}/sections/{sectionId}` Scope: `studio:write` · Retries: reuse the same `client_mutation_id` · Operation ID: `moveStudioSection` Moves a section to a new position in the project. Send the current `section_order_version`; a stale version returns `409`. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | | `sectionId` | string, uuid | Yes | | #### Request body (application/json, required) - `schema_version` (1; required) - `actor_session_id` (string, uuid; required) - `client_mutation_id` (string, uuid; required): Idempotency key; a replay with the same canonical request returns the committed result. - `base_section_order_version` (integer, ≥ 1; required) - `to_index` (integer, 0–4999; required): Zero-based target index in the active section order. #### Response 200 Section move committed or replayed. Fields inside `data`: - `operation` (string, one of `move_section`, `delete_section`; required): move_section for PATCH and delete_section for DELETE. - `project_id` (string, uuid; required) - `section_id` (string, uuid; required) - `section_order_version` (integer, ≥ 1; required): Section-order version committed by this mutation. - `idempotent_replay` (boolean; required) Errors: `400`, `401`, `403`, `404`, `409`, `500` return the standard error envelope. #### Example request ```bash curl -X PATCH "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/sections/$SECTION_ID" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schema_version": 1, "actor_session_id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10", "client_mutation_id": "c9f0f895-fb98-4b91-9e3f-1d2a6b7c8d20", "base_section_order_version": 1, "to_index": 1 }' ``` ### Preview section deletion `GET /studio/projects/{projectId}/sections/{sectionId}/deletion-info` Scope: `studio:read` · Operation ID: `getStudioSectionDeletionInfo` Returns counts of the script blocks, generated clips, and active share links that deleting the section would make inaccessible. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | | `sectionId` | string, uuid | Yes | | #### Response 200 Section deletion preview. Fields inside `data`: - `project_id` (string, uuid; required) - `section_id` (string, uuid; required) - `block_count` (integer, ≥ 0; required) - `generated_clip_count` (integer, ≥ 0; required) - `active_share_link_count` (integer, ≥ 0; required) Errors: `400`, `401`, `403`, `404`, `500` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/sections/$SECTION_ID/deletion-info" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Delete a section `DELETE /studio/projects/{projectId}/sections/{sectionId}` Scope: `studio:write` · Retries: reuse the same `client_mutation_id` · Operation ID: `deleteStudioSection` Deletes a section after checking both the section order version and the document version. This cannot be undone through the API. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | | `sectionId` | string, uuid | Yes | | #### Request body (application/json, required) - `schema_version` (1; required) - `actor_session_id` (string, uuid; required) - `client_mutation_id` (string, uuid; required): Idempotency key; a replay with the same canonical request returns the committed result. - `base_section_order_version` (integer, ≥ 1; required) - `base_document_version` (integer, ≥ 1; required) #### Response 200 Section deletion committed or replayed. Fields inside `data`: - `operation` (string, one of `move_section`, `delete_section`; required): move_section for PATCH and delete_section for DELETE. - `project_id` (string, uuid; required) - `section_id` (string, uuid; required) - `section_order_version` (integer, ≥ 1; required): Section-order version committed by this mutation. - `idempotent_replay` (boolean; required) Errors: `400`, `401`, `403`, `404`, `409`, `500` return the standard error envelope. #### Example request ```bash curl -X DELETE "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/sections/$SECTION_ID" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schema_version": 1, "actor_session_id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10", "client_mutation_id": "c9f0f895-fb98-4b91-9e3f-1d2a6b7c8d20", "base_section_order_version": 1, "base_document_version": 1 }' ``` --- Source: https://lyricwinter.com/docs/api/documents.md --- title: "Documents and speakers API" description: "Every section has a versioned document of narration and dialogue blocks attributed to speakers. Read the document before editing it or generating audio, and list the project's speakers and casting." canonical_url: https://lyricwinter.com/docs/api/documents markdown_url: https://lyricwinter.com/docs/api/documents.md last_updated: 2026-10-01 status: beta --- # LyricWinter API reference: Documents and speakers > Every section has a versioned document of narration and dialogue blocks attributed to speakers. Read the document before editing it or generating audio, and list the project's speakers and casting. The LyricWinter API is in beta. Base URL: `https://lyricwinter.com/api/v1`. Authenticate every request with `Authorization: Bearer lw_...`. Successful responses are wrapped as `{ "data": ..., "request_id": "req_..." }`. Machine-readable contract: https://lyricwinter.com/api/v1/openapi.json. ## Endpoints - [Get a section document](https://lyricwinter.com/docs/api/documents#get-studio-document): `GET /studio/sections/{sectionId}/document` - [Edit a section document](https://lyricwinter.com/docs/api/documents#save-studio-document): `PATCH /studio/sections/{sectionId}/document` - [Import a speaker-labeled script](https://lyricwinter.com/docs/api/documents#import-studio-script): `POST /studio/sections/{sectionId}/script` - [List project speakers](https://lyricwinter.com/docs/api/documents#get-studio-project-speaker-usage): `GET /studio/projects/{projectId}/speakers` - [Assign a speaker's voice and model](https://lyricwinter.com/docs/api/documents#update-studio-project-speaker-casting): `PATCH /studio/projects/{projectId}/speakers/{speakerId}/casting` ### Get a section document `GET /studio/sections/{sectionId}/document` Scope: `studio:read` · Operation ID: `getStudioDocument` Returns the section's current document: its `document_version`, blocks, the project's speakers and `speaker_registry_version`, and whether section sound effects are enabled. Read it before editing the section or generating audio. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `sectionId` | string, uuid | Yes | | #### Response 200 Canonical Studio document and optional recovery draft. Fields inside `data`: - `actor_user_id` (string, uuid; required): Authenticated caller; clients partition local recovery state by this ID. - `project_id` (string, uuid; required) - `document` (object; required): The canonical structured script of one section (studio-ast-2): its versions, speakers, and ordered blocks. - `blocks` (array of object, max 100000 items; required) - One of: - **kind: "speech"** - `block_version` (integer, > 0; required) - `content` (object; required) - `annotations` (array of object; required) - One of: - **kind: "exclusion"** - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("exclusion"; required) - `reason` (string, one of `speaker_label`, `stage_direction`, `metadata`, `user`; required) - `start_utf16` (integer, ≥ 0; required) - **kind: "pronunciation"** - `alphabet` (string, one of `ipa`, `x-sampa`, `cmu`, `alias`; required) - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("pronunciation"; required) - `start_utf16` (integer, ≥ 0; required) - `value` (string, 1–500 chars; required) - **kind: "extension"** - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("extension"; required) - `name` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `namespace` (string, pattern ^[a-z][a-z0-9_.-]{0,63}$; required) - `payload` (object | null; required) - `start_utf16` (integer, ≥ 0; required) - `version` (integer, > 0; required) - `delivery_control_sets` (array of object, max 32 items; required) - `capability_revision` (string, 1–200 chars; required) - `directives` (array of StudioAstDeliveryDirective, max 256 items; required) - `format` (string, one of `eleven-v4-1`, `inworld-tts2-1`, `inworld-tts2-experimental-1`, `inworld-tts15-max-1`, `inworld-tts15-mini-1`, `fish-s2-inline-1`, `fish-s2.1-inline-1`, `cartesia-sonic36-1`, `cartesia-sonic35-1`, `openai-gpt4o-mini-tts-1`, `openai-tts1-hd-1`, `openai-tts1-1`, `smallest-lightning31-1`, `chatterbox-classic-1`, `zyphra-zonos2-cloud-1`, `f5tts-v1-1`, `misotts-8b-1`; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `model` (string, 1–200 chars; required) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `provider_text_override` (string, 1–65536 chars, pattern [\s\S]*\S[\s\S]*; optional) - `schema_version` (string, one of `studio-delivery-control-set-1`, `studio-delivery-control-set-2`; required) - `settings` (array of StudioAstDeliverySetting, max 32 items; required) - `source_fingerprint` (string | null, pattern ^[0-9a-f]{64}$; required) - `source_utf16_length` (integer | null, ≥ 0; required) - `points` (array of object; required) - One of: - **kind: "timed_silence"** - `duration_ms` (integer, 1–300000; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("timed_silence"; required) - `offset_utf16` (integer, ≥ 0; required) - **kind: "extension"** - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("extension"; required) - `name` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `namespace` (string, pattern ^[a-z][a-z0-9_.-]{0,63}$; required) - `offset_utf16` (integer, ≥ 0; required) - `payload` (object | null; required) - `version` (integer, > 0; required) - `schema_version` ("studio-ast-2"; required) - `text` (string, max 65536 chars; required): Exact block text; at most 65,536 UTF-16 code units. Annotation, point, and delivery offsets are UTF-16 offsets into this string. - `type` ("text"; required) - `generation_profile_override` (object | null; required): Partial saved profile. New document mutations validate the effective inherited provider/model and options against StudioGenerationProfileRequest. The read schema stays permissive for historical profiles. Join options with speaker context; an override is not a standalone full profile. - `controls` (string, one of `strict`, `best_effort`; optional) - `fallback` (string, one of `strict`, `best_effort`; optional) - `model` (string, 1–200 chars; optional) - `options` (map of boolean | string | integer | number | array of StudioAstJsonValue | map of StudioAstJsonValue | null; optional) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; optional) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("speech"; required) - `placement` (any | null; required) - `speaker_id` (string | null, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - **kind: "narration"** - `block_version` (integer, > 0; required) - `content` (object; required) - `annotations` (array of object; required) - One of: - **kind: "exclusion"** - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("exclusion"; required) - `reason` (string, one of `speaker_label`, `stage_direction`, `metadata`, `user`; required) - `start_utf16` (integer, ≥ 0; required) - **kind: "pronunciation"** - `alphabet` (string, one of `ipa`, `x-sampa`, `cmu`, `alias`; required) - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("pronunciation"; required) - `start_utf16` (integer, ≥ 0; required) - `value` (string, 1–500 chars; required) - **kind: "extension"** - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("extension"; required) - `name` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `namespace` (string, pattern ^[a-z][a-z0-9_.-]{0,63}$; required) - `payload` (object | null; required) - `start_utf16` (integer, ≥ 0; required) - `version` (integer, > 0; required) - `delivery_control_sets` (array of object, max 32 items; required) - `capability_revision` (string, 1–200 chars; required) - `directives` (array of StudioAstDeliveryDirective, max 256 items; required) - `format` (string, one of `eleven-v4-1`, `inworld-tts2-1`, `inworld-tts2-experimental-1`, `inworld-tts15-max-1`, `inworld-tts15-mini-1`, `fish-s2-inline-1`, `fish-s2.1-inline-1`, `cartesia-sonic36-1`, `cartesia-sonic35-1`, `openai-gpt4o-mini-tts-1`, `openai-tts1-hd-1`, `openai-tts1-1`, `smallest-lightning31-1`, `chatterbox-classic-1`, `zyphra-zonos2-cloud-1`, `f5tts-v1-1`, `misotts-8b-1`; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `model` (string, 1–200 chars; required) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `provider_text_override` (string, 1–65536 chars, pattern [\s\S]*\S[\s\S]*; optional) - `schema_version` (string, one of `studio-delivery-control-set-1`, `studio-delivery-control-set-2`; required) - `settings` (array of StudioAstDeliverySetting, max 32 items; required) - `source_fingerprint` (string | null, pattern ^[0-9a-f]{64}$; required) - `source_utf16_length` (integer | null, ≥ 0; required) - `points` (array of object; required) - One of: - **kind: "timed_silence"** - `duration_ms` (integer, 1–300000; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("timed_silence"; required) - `offset_utf16` (integer, ≥ 0; required) - **kind: "extension"** - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("extension"; required) - `name` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `namespace` (string, pattern ^[a-z][a-z0-9_.-]{0,63}$; required) - `offset_utf16` (integer, ≥ 0; required) - `payload` (object | null; required) - `version` (integer, > 0; required) - `schema_version` ("studio-ast-2"; required) - `text` (string, max 65536 chars; required): Exact block text; at most 65,536 UTF-16 code units. Annotation, point, and delivery offsets are UTF-16 offsets into this string. - `type` ("text"; required) - `generation_profile_override` (object | null; required): Partial saved profile. New document mutations validate the effective inherited provider/model and options against StudioGenerationProfileRequest. The read schema stays permissive for historical profiles. Join options with speaker context; an override is not a standalone full profile. - `controls` (string, one of `strict`, `best_effort`; optional) - `fallback` (string, one of `strict`, `best_effort`; optional) - `model` (string, 1–200 chars; optional) - `options` (map of boolean | string | integer | number | array of StudioAstJsonValue | map of StudioAstJsonValue | null; optional) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; optional) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("narration"; required) - `placement` (any | null; required) - `speaker_id` (string | null, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - **kind: "unparsed"** - `block_version` (integer, > 0; required) - `content` (object; required) - `annotations` (array of object; required) - One of: - **kind: "exclusion"** - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("exclusion"; required) - `reason` (string, one of `speaker_label`, `stage_direction`, `metadata`, `user`; required) - `start_utf16` (integer, ≥ 0; required) - **kind: "pronunciation"** - `alphabet` (string, one of `ipa`, `x-sampa`, `cmu`, `alias`; required) - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("pronunciation"; required) - `start_utf16` (integer, ≥ 0; required) - `value` (string, 1–500 chars; required) - **kind: "extension"** - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("extension"; required) - `name` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `namespace` (string, pattern ^[a-z][a-z0-9_.-]{0,63}$; required) - `payload` (object | null; required) - `start_utf16` (integer, ≥ 0; required) - `version` (integer, > 0; required) - `delivery_control_sets` (array of object, max 32 items; required) - `capability_revision` (string, 1–200 chars; required) - `directives` (array of StudioAstDeliveryDirective, max 256 items; required) - `format` (string, one of `eleven-v4-1`, `inworld-tts2-1`, `inworld-tts2-experimental-1`, `inworld-tts15-max-1`, `inworld-tts15-mini-1`, `fish-s2-inline-1`, `fish-s2.1-inline-1`, `cartesia-sonic36-1`, `cartesia-sonic35-1`, `openai-gpt4o-mini-tts-1`, `openai-tts1-hd-1`, `openai-tts1-1`, `smallest-lightning31-1`, `chatterbox-classic-1`, `zyphra-zonos2-cloud-1`, `f5tts-v1-1`, `misotts-8b-1`; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `model` (string, 1–200 chars; required) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `provider_text_override` (string, 1–65536 chars, pattern [\s\S]*\S[\s\S]*; optional) - `schema_version` (string, one of `studio-delivery-control-set-1`, `studio-delivery-control-set-2`; required) - `settings` (array of StudioAstDeliverySetting, max 32 items; required) - `source_fingerprint` (string | null, pattern ^[0-9a-f]{64}$; required) - `source_utf16_length` (integer | null, ≥ 0; required) - `points` (array of object; required) - One of: - **kind: "timed_silence"** - `duration_ms` (integer, 1–300000; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("timed_silence"; required) - `offset_utf16` (integer, ≥ 0; required) - **kind: "extension"** - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("extension"; required) - `name` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `namespace` (string, pattern ^[a-z][a-z0-9_.-]{0,63}$; required) - `offset_utf16` (integer, ≥ 0; required) - `payload` (object | null; required) - `version` (integer, > 0; required) - `schema_version` ("studio-ast-2"; required) - `text` (string, max 65536 chars; required): Exact block text; at most 65,536 UTF-16 code units. Annotation, point, and delivery offsets are UTF-16 offsets into this string. - `type` ("text"; required) - `generation_profile_override` (object | null; required): Partial saved profile. New document mutations validate the effective inherited provider/model and options against StudioGenerationProfileRequest. The read schema stays permissive for historical profiles. Join options with speaker context; an override is not a standalone full profile. - `controls` (string, one of `strict`, `best_effort`; optional) - `fallback` (string, one of `strict`, `best_effort`; optional) - `model` (string, 1–200 chars; optional) - `options` (map of boolean | string | integer | number | array of StudioAstJsonValue | map of StudioAstJsonValue | null; optional) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; optional) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("unparsed"; required) - `placement` (any | null; required) - `speaker_id` (any | null; required) - **kind: "excluded"** - `block_version` (integer, > 0; required) - `content` (object; required) - `annotations` (array of object; required) - One of: - **kind: "exclusion"** - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("exclusion"; required) - `reason` (string, one of `speaker_label`, `stage_direction`, `metadata`, `user`; required) - `start_utf16` (integer, ≥ 0; required) - **kind: "pronunciation"** - `alphabet` (string, one of `ipa`, `x-sampa`, `cmu`, `alias`; required) - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("pronunciation"; required) - `start_utf16` (integer, ≥ 0; required) - `value` (string, 1–500 chars; required) - **kind: "extension"** - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("extension"; required) - `name` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `namespace` (string, pattern ^[a-z][a-z0-9_.-]{0,63}$; required) - `payload` (object | null; required) - `start_utf16` (integer, ≥ 0; required) - `version` (integer, > 0; required) - `delivery_control_sets` (array of object, max 32 items; required) - `capability_revision` (string, 1–200 chars; required) - `directives` (array of StudioAstDeliveryDirective, max 256 items; required) - `format` (string, one of `eleven-v4-1`, `inworld-tts2-1`, `inworld-tts2-experimental-1`, `inworld-tts15-max-1`, `inworld-tts15-mini-1`, `fish-s2-inline-1`, `fish-s2.1-inline-1`, `cartesia-sonic36-1`, `cartesia-sonic35-1`, `openai-gpt4o-mini-tts-1`, `openai-tts1-hd-1`, `openai-tts1-1`, `smallest-lightning31-1`, `chatterbox-classic-1`, `zyphra-zonos2-cloud-1`, `f5tts-v1-1`, `misotts-8b-1`; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `model` (string, 1–200 chars; required) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `provider_text_override` (string, 1–65536 chars, pattern [\s\S]*\S[\s\S]*; optional) - `schema_version` (string, one of `studio-delivery-control-set-1`, `studio-delivery-control-set-2`; required) - `settings` (array of StudioAstDeliverySetting, max 32 items; required) - `source_fingerprint` (string | null, pattern ^[0-9a-f]{64}$; required) - `source_utf16_length` (integer | null, ≥ 0; required) - `points` (array of object; required) - One of: - **kind: "timed_silence"** - `duration_ms` (integer, 1–300000; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("timed_silence"; required) - `offset_utf16` (integer, ≥ 0; required) - **kind: "extension"** - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("extension"; required) - `name` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `namespace` (string, pattern ^[a-z][a-z0-9_.-]{0,63}$; required) - `offset_utf16` (integer, ≥ 0; required) - `payload` (object | null; required) - `version` (integer, > 0; required) - `schema_version` ("studio-ast-2"; required) - `text` (string, max 65536 chars; required): Exact block text; at most 65,536 UTF-16 code units. Annotation, point, and delivery offsets are UTF-16 offsets into this string. - `type` ("text"; required) - `generation_profile_override` (object | null; required): Partial saved profile. New document mutations validate the effective inherited provider/model and options against StudioGenerationProfileRequest. The read schema stays permissive for historical profiles. Join options with speaker context; an override is not a standalone full profile. - `controls` (string, one of `strict`, `best_effort`; optional) - `fallback` (string, one of `strict`, `best_effort`; optional) - `model` (string, 1–200 chars; optional) - `options` (map of boolean | string | integer | number | array of StudioAstJsonValue | map of StudioAstJsonValue | null; optional) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; optional) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("excluded"; required) - `placement` (any | null; required) - `speaker_id` (any | null; required) - **kind: "note"** - `block_version` (integer, > 0; required) - `content` (object; required) - `schema_version` ("studio-ast-2"; required) - `text` (string, max 20000 chars; required) - `type` ("note"; required) - `generation_profile_override` (object | null; required): Partial saved profile. New document mutations validate the effective inherited provider/model and options against StudioGenerationProfileRequest. The read schema stays permissive for historical profiles. Join options with speaker context; an override is not a standalone full profile. - `controls` (string, one of `strict`, `best_effort`; optional) - `fallback` (string, one of `strict`, `best_effort`; optional) - `model` (string, 1–200 chars; optional) - `options` (map of boolean | string | integer | number | array of StudioAstJsonValue | map of StudioAstJsonValue | null; optional) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; optional) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("note"; required) - `placement` (any | null; required) - `speaker_id` (any | null; required) - **kind: "sfx"** - `block_version` (integer, > 0; required) - `content` (object; required) - `duration_ms` (integer, 100–3900000; required): Generated sound effects (no source) allow up to 300000 ms; a pinned project sound allows up to 3900000 ms. - `prompt` (string, 1–4000 chars; required) - `schema_version` ("studio-ast-2"; required) - `seed` (integer | null, ≥ -9007199254740991; required) - `type` ("sfx"; required) - `source` (object; optional): Present only when the SFX block plays an uploaded project sound; seed is then null. - `kind` ("project_sound"; required) - `sound_effect_id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `sound_effect_revision_id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `generation_profile_override` (object | null; required): Partial saved profile. New document mutations validate the effective inherited provider/model and options against StudioGenerationProfileRequest. The read schema stays permissive for historical profiles. Join options with speaker context; an override is not a standalone full profile. - `controls` (string, one of `strict`, `best_effort`; optional) - `fallback` (string, one of `strict`, `best_effort`; optional) - `model` (string, 1–200 chars; optional) - `options` (map of boolean | string | integer | number | array of StudioAstJsonValue | map of StudioAstJsonValue | null; optional) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; optional) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("sfx"; required) - `placement` (object; required) - `anchor` (object; required) - One of: - **kind: "block"** - `block_id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `fraction` (number | null, 0–1; required) - `kind` ("block"; required) - `position` (string, one of `start`, `fraction`, `end`, `after`; required) - **kind: "section"** - `kind` ("section"; required) - `position` (string, one of `start`, `end`; required) - **kind: "unplaced"** - `kind` ("unplaced"; required) - `fade_in_ms` (integer, 0–300000; required) - `fade_out_ms` (integer, 0–300000; required) - `gain_db` (number, -60–24; required) - `offset_ms` (integer, -300000–300000; required) - `speaker_id` (any | null; required) - `different_speaker_gap_ms` (integer, 0–1000; required): Silence in milliseconds inserted between adjacent spoken blocks with different speakers. - `document_version` (integer, > 0; required) - `sfx_enabled` (boolean; required): Whether authored section sound effects are audible in playback and included in rendered exports. Legacy sections without a stored value report true. - `same_speaker_gap_ms` (integer, 0–1000; required): Silence in milliseconds inserted between adjacent spoken blocks with the same speaker. - `schema_version` ("studio-ast-2"; required) - `section_id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required): Canonical lowercase UUID of the section. - `speaker_registry_version` (integer, > 0; required): Project-wide speaker registry version the speakers list was read at. - `speakers` (array of object; required): Every active speaker in the project registry, not only speakers referenced by this section. - `generation_profile` (object | null; required): Saved profile read shape, including historical options. For new full-profile requests use StudioGenerationProfileRequest; its provider/model branches list accepted option keys. Generation eligibility and actual audio readiness are separate reads. - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `model` (string, 1–200 chars; required) - `fallback` (string, one of `strict`, `best_effort`; required) - `controls` (string, one of `strict`, `best_effort`; required) - `options` (object; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `name` (string, 1–200 chars; required) - `speaker_version` (integer, > 0; required) - `voice_id` (string | null, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `title` (string, max 500 chars; required) - `source_draft` (object | null; required): The caller's saved, uncommitted Advanced Source draft for this section, or null. - `source_text` (string; required) - `base_document_version` (integer, ≥ 1; required) - `base_speaker_registry_version` (integer, ≥ 1; required) - `base_speaker_versions` (map of integer; required): Speaker versions keyed by speaker ID when the draft began. - `diagnostics` (array of object, max 1000 items; required) - `code` (string, one of `INVALID_SOURCE`, `STALE_SOURCE`; required) - `message` (string, 1–2000 chars; required) - `updated_at` (string, date-time; required) Errors: `400`, `401`, `403`, `404`, `500` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/sections/$SECTION_ID/document" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Edit a section document `PATCH /studio/sections/{sectionId}/document` Scope: `studio:write` · Retries: reuse the same `client_mutation_id` · Operation ID: `saveStudioDocument` Applies a batch of document commands against the versions you last read. The whole batch commits atomically; if any targeted version changed, the request fails with `409` and nothing is applied. For an existing pinned project-sound block, `content.duration_ms` must equal its immutable source revision's duration; changing it returns `400 STUDIO_PROJECT_SOUND_DURATION_MISMATCH` with expected_duration_ms and requested_duration_ms. Placement gain/fades do not trim the source or shorten the timeline. To change only the gaps between passages, send `set_section_pacing` with integer `same_speaker_gap_ms` and `different_speaker_gap_ms` from 0 through 1000; use empty target-version maps for this section-only command. This preserves the script and existing takes. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `sectionId` | string, uuid | Yes | | #### Request body (application/json, required) - `schema_version` (1; required) - `actor_session_id` (string, uuid; required) - `client_mutation_id` (string, uuid; required) - `base_document_version` (integer, ≥ 1; required) - `base_speaker_registry_version` (integer, ≥ 1; required) - `target_block_versions` (map of integer; required) - `target_speaker_versions` (map of integer; required) - `commands` (array of object, 1–1000 items; required) - One of: - **kind: "set_title"** - `kind` ("set_title"; required) - `title` (string, max 500 chars; required) - **kind: "set_section_pacing"**: Set silence between consecutive audible passages without replacing the script. Both gaps are required; normal document and cast version checks apply. - `kind` ("set_section_pacing"; required) - `same_speaker_gap_ms` (integer, 0–1000; required) - `different_speaker_gap_ms` (integer, 0–1000; required) - **kind: "set_section_sfx_enabled"** - `kind` ("set_section_sfx_enabled"; required) - `sfx_enabled` (boolean; required): Whether authored section sound effects are audible in playback and included in rendered exports. Disabling preserves authored SFX blocks and renditions. - **kind: "replace_text"**: Offsets are UTF-16 indices. Runtime also verifies the expected text hash and range against the current block. - `kind` ("replace_text"; required) - `block_id` (string, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - `start_utf16` (integer, ≥ 0; required) - `end_utf16` (integer, ≥ 0; required) - `expected_text_hash` (string, pattern ^[0-9a-f]{64}$; required) - `text` (string; required) - **kind: "insert_block"** - `kind` ("insert_block"; required) - `index` (integer, ≥ 0; required) - `block` (object; required) - One of: - **kind: "speech"** - `block_version` (integer, > 0; required) - `content` (object; required) - `annotations` (array of object; required) - One of: - **kind: "exclusion"** - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("exclusion"; required) - `reason` (string, one of `speaker_label`, `stage_direction`, `metadata`, `user`; required) - `start_utf16` (integer, ≥ 0; required) - **kind: "pronunciation"** - `alphabet` (string, one of `ipa`, `x-sampa`, `cmu`, `alias`; required) - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("pronunciation"; required) - `start_utf16` (integer, ≥ 0; required) - `value` (string, 1–500 chars; required) - **kind: "extension"** - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("extension"; required) - `name` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `namespace` (string, pattern ^[a-z][a-z0-9_.-]{0,63}$; required) - `payload` (object | null; required) - `start_utf16` (integer, ≥ 0; required) - `version` (integer, > 0; required) - `delivery_control_sets` (array of object, max 32 items; required) - `capability_revision` (string, 1–200 chars; required) - `directives` (array of StudioAstDeliveryDirective, max 256 items; required) - `format` (string, one of `eleven-v4-1`, `inworld-tts2-1`, `inworld-tts2-experimental-1`, `inworld-tts15-max-1`, `inworld-tts15-mini-1`, `fish-s2-inline-1`, `fish-s2.1-inline-1`, `cartesia-sonic36-1`, `cartesia-sonic35-1`, `openai-gpt4o-mini-tts-1`, `openai-tts1-hd-1`, `openai-tts1-1`, `smallest-lightning31-1`, `chatterbox-classic-1`, `zyphra-zonos2-cloud-1`, `f5tts-v1-1`, `misotts-8b-1`; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `model` (string, 1–200 chars; required) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `provider_text_override` (string, 1–65536 chars, pattern [\s\S]*\S[\s\S]*; optional) - `schema_version` (string, one of `studio-delivery-control-set-1`, `studio-delivery-control-set-2`; required) - `settings` (array of StudioAstDeliverySetting, max 32 items; required) - `source_fingerprint` (string | null, pattern ^[0-9a-f]{64}$; required) - `source_utf16_length` (integer | null, ≥ 0; required) - `points` (array of object; required) - One of: - **kind: "timed_silence"** - `duration_ms` (integer, 1–300000; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("timed_silence"; required) - `offset_utf16` (integer, ≥ 0; required) - **kind: "extension"** - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("extension"; required) - `name` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `namespace` (string, pattern ^[a-z][a-z0-9_.-]{0,63}$; required) - `offset_utf16` (integer, ≥ 0; required) - `payload` (object | null; required) - `version` (integer, > 0; required) - `schema_version` ("studio-ast-2"; required) - `text` (string, max 65536 chars; required): Exact block text; at most 65,536 UTF-16 code units. Annotation, point, and delivery offsets are UTF-16 offsets into this string. - `type` ("text"; required) - `generation_profile_override` (object | null; required): Partial saved profile. New document mutations validate the effective inherited provider/model and options against StudioGenerationProfileRequest. The read schema stays permissive for historical profiles. Join options with speaker context; an override is not a standalone full profile. - `controls` (string, one of `strict`, `best_effort`; optional) - `fallback` (string, one of `strict`, `best_effort`; optional) - `model` (string, 1–200 chars; optional) - `options` (map of boolean | string | integer | number | array of StudioAstJsonValue | map of StudioAstJsonValue | null; optional) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; optional) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("speech"; required) - `placement` (any | null; required) - `speaker_id` (string | null, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - **kind: "narration"** - `block_version` (integer, > 0; required) - `content` (object; required) - `annotations` (array of object; required) - One of: - **kind: "exclusion"** - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("exclusion"; required) - `reason` (string, one of `speaker_label`, `stage_direction`, `metadata`, `user`; required) - `start_utf16` (integer, ≥ 0; required) - **kind: "pronunciation"** - `alphabet` (string, one of `ipa`, `x-sampa`, `cmu`, `alias`; required) - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("pronunciation"; required) - `start_utf16` (integer, ≥ 0; required) - `value` (string, 1–500 chars; required) - **kind: "extension"** - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("extension"; required) - `name` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `namespace` (string, pattern ^[a-z][a-z0-9_.-]{0,63}$; required) - `payload` (object | null; required) - `start_utf16` (integer, ≥ 0; required) - `version` (integer, > 0; required) - `delivery_control_sets` (array of object, max 32 items; required) - `capability_revision` (string, 1–200 chars; required) - `directives` (array of StudioAstDeliveryDirective, max 256 items; required) - `format` (string, one of `eleven-v4-1`, `inworld-tts2-1`, `inworld-tts2-experimental-1`, `inworld-tts15-max-1`, `inworld-tts15-mini-1`, `fish-s2-inline-1`, `fish-s2.1-inline-1`, `cartesia-sonic36-1`, `cartesia-sonic35-1`, `openai-gpt4o-mini-tts-1`, `openai-tts1-hd-1`, `openai-tts1-1`, `smallest-lightning31-1`, `chatterbox-classic-1`, `zyphra-zonos2-cloud-1`, `f5tts-v1-1`, `misotts-8b-1`; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `model` (string, 1–200 chars; required) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `provider_text_override` (string, 1–65536 chars, pattern [\s\S]*\S[\s\S]*; optional) - `schema_version` (string, one of `studio-delivery-control-set-1`, `studio-delivery-control-set-2`; required) - `settings` (array of StudioAstDeliverySetting, max 32 items; required) - `source_fingerprint` (string | null, pattern ^[0-9a-f]{64}$; required) - `source_utf16_length` (integer | null, ≥ 0; required) - `points` (array of object; required) - One of: - **kind: "timed_silence"** - `duration_ms` (integer, 1–300000; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("timed_silence"; required) - `offset_utf16` (integer, ≥ 0; required) - **kind: "extension"** - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("extension"; required) - `name` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `namespace` (string, pattern ^[a-z][a-z0-9_.-]{0,63}$; required) - `offset_utf16` (integer, ≥ 0; required) - `payload` (object | null; required) - `version` (integer, > 0; required) - `schema_version` ("studio-ast-2"; required) - `text` (string, max 65536 chars; required): Exact block text; at most 65,536 UTF-16 code units. Annotation, point, and delivery offsets are UTF-16 offsets into this string. - `type` ("text"; required) - `generation_profile_override` (object | null; required): Partial saved profile. New document mutations validate the effective inherited provider/model and options against StudioGenerationProfileRequest. The read schema stays permissive for historical profiles. Join options with speaker context; an override is not a standalone full profile. - `controls` (string, one of `strict`, `best_effort`; optional) - `fallback` (string, one of `strict`, `best_effort`; optional) - `model` (string, 1–200 chars; optional) - `options` (map of boolean | string | integer | number | array of StudioAstJsonValue | map of StudioAstJsonValue | null; optional) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; optional) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("narration"; required) - `placement` (any | null; required) - `speaker_id` (string | null, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - **kind: "unparsed"** - `block_version` (integer, > 0; required) - `content` (object; required) - `annotations` (array of object; required) - One of: - **kind: "exclusion"** - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("exclusion"; required) - `reason` (string, one of `speaker_label`, `stage_direction`, `metadata`, `user`; required) - `start_utf16` (integer, ≥ 0; required) - **kind: "pronunciation"** - `alphabet` (string, one of `ipa`, `x-sampa`, `cmu`, `alias`; required) - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("pronunciation"; required) - `start_utf16` (integer, ≥ 0; required) - `value` (string, 1–500 chars; required) - **kind: "extension"** - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("extension"; required) - `name` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `namespace` (string, pattern ^[a-z][a-z0-9_.-]{0,63}$; required) - `payload` (object | null; required) - `start_utf16` (integer, ≥ 0; required) - `version` (integer, > 0; required) - `delivery_control_sets` (array of object, max 32 items; required) - `capability_revision` (string, 1–200 chars; required) - `directives` (array of StudioAstDeliveryDirective, max 256 items; required) - `format` (string, one of `eleven-v4-1`, `inworld-tts2-1`, `inworld-tts2-experimental-1`, `inworld-tts15-max-1`, `inworld-tts15-mini-1`, `fish-s2-inline-1`, `fish-s2.1-inline-1`, `cartesia-sonic36-1`, `cartesia-sonic35-1`, `openai-gpt4o-mini-tts-1`, `openai-tts1-hd-1`, `openai-tts1-1`, `smallest-lightning31-1`, `chatterbox-classic-1`, `zyphra-zonos2-cloud-1`, `f5tts-v1-1`, `misotts-8b-1`; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `model` (string, 1–200 chars; required) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `provider_text_override` (string, 1–65536 chars, pattern [\s\S]*\S[\s\S]*; optional) - `schema_version` (string, one of `studio-delivery-control-set-1`, `studio-delivery-control-set-2`; required) - `settings` (array of StudioAstDeliverySetting, max 32 items; required) - `source_fingerprint` (string | null, pattern ^[0-9a-f]{64}$; required) - `source_utf16_length` (integer | null, ≥ 0; required) - `points` (array of object; required) - One of: - **kind: "timed_silence"** - `duration_ms` (integer, 1–300000; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("timed_silence"; required) - `offset_utf16` (integer, ≥ 0; required) - **kind: "extension"** - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("extension"; required) - `name` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `namespace` (string, pattern ^[a-z][a-z0-9_.-]{0,63}$; required) - `offset_utf16` (integer, ≥ 0; required) - `payload` (object | null; required) - `version` (integer, > 0; required) - `schema_version` ("studio-ast-2"; required) - `text` (string, max 65536 chars; required): Exact block text; at most 65,536 UTF-16 code units. Annotation, point, and delivery offsets are UTF-16 offsets into this string. - `type` ("text"; required) - `generation_profile_override` (object | null; required): Partial saved profile. New document mutations validate the effective inherited provider/model and options against StudioGenerationProfileRequest. The read schema stays permissive for historical profiles. Join options with speaker context; an override is not a standalone full profile. - `controls` (string, one of `strict`, `best_effort`; optional) - `fallback` (string, one of `strict`, `best_effort`; optional) - `model` (string, 1–200 chars; optional) - `options` (map of boolean | string | integer | number | array of StudioAstJsonValue | map of StudioAstJsonValue | null; optional) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; optional) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("unparsed"; required) - `placement` (any | null; required) - `speaker_id` (any | null; required) - **kind: "excluded"** - `block_version` (integer, > 0; required) - `content` (object; required) - `annotations` (array of object; required) - One of: - **kind: "exclusion"** - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("exclusion"; required) - `reason` (string, one of `speaker_label`, `stage_direction`, `metadata`, `user`; required) - `start_utf16` (integer, ≥ 0; required) - **kind: "pronunciation"** - `alphabet` (string, one of `ipa`, `x-sampa`, `cmu`, `alias`; required) - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("pronunciation"; required) - `start_utf16` (integer, ≥ 0; required) - `value` (string, 1–500 chars; required) - **kind: "extension"** - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("extension"; required) - `name` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `namespace` (string, pattern ^[a-z][a-z0-9_.-]{0,63}$; required) - `payload` (object | null; required) - `start_utf16` (integer, ≥ 0; required) - `version` (integer, > 0; required) - `delivery_control_sets` (array of object, max 32 items; required) - `capability_revision` (string, 1–200 chars; required) - `directives` (array of StudioAstDeliveryDirective, max 256 items; required) - `format` (string, one of `eleven-v4-1`, `inworld-tts2-1`, `inworld-tts2-experimental-1`, `inworld-tts15-max-1`, `inworld-tts15-mini-1`, `fish-s2-inline-1`, `fish-s2.1-inline-1`, `cartesia-sonic36-1`, `cartesia-sonic35-1`, `openai-gpt4o-mini-tts-1`, `openai-tts1-hd-1`, `openai-tts1-1`, `smallest-lightning31-1`, `chatterbox-classic-1`, `zyphra-zonos2-cloud-1`, `f5tts-v1-1`, `misotts-8b-1`; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `model` (string, 1–200 chars; required) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `provider_text_override` (string, 1–65536 chars, pattern [\s\S]*\S[\s\S]*; optional) - `schema_version` (string, one of `studio-delivery-control-set-1`, `studio-delivery-control-set-2`; required) - `settings` (array of StudioAstDeliverySetting, max 32 items; required) - `source_fingerprint` (string | null, pattern ^[0-9a-f]{64}$; required) - `source_utf16_length` (integer | null, ≥ 0; required) - `points` (array of object; required) - One of: - **kind: "timed_silence"** - `duration_ms` (integer, 1–300000; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("timed_silence"; required) - `offset_utf16` (integer, ≥ 0; required) - **kind: "extension"** - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("extension"; required) - `name` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `namespace` (string, pattern ^[a-z][a-z0-9_.-]{0,63}$; required) - `offset_utf16` (integer, ≥ 0; required) - `payload` (object | null; required) - `version` (integer, > 0; required) - `schema_version` ("studio-ast-2"; required) - `text` (string, max 65536 chars; required): Exact block text; at most 65,536 UTF-16 code units. Annotation, point, and delivery offsets are UTF-16 offsets into this string. - `type` ("text"; required) - `generation_profile_override` (object | null; required): Partial saved profile. New document mutations validate the effective inherited provider/model and options against StudioGenerationProfileRequest. The read schema stays permissive for historical profiles. Join options with speaker context; an override is not a standalone full profile. - `controls` (string, one of `strict`, `best_effort`; optional) - `fallback` (string, one of `strict`, `best_effort`; optional) - `model` (string, 1–200 chars; optional) - `options` (map of boolean | string | integer | number | array of StudioAstJsonValue | map of StudioAstJsonValue | null; optional) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; optional) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("excluded"; required) - `placement` (any | null; required) - `speaker_id` (any | null; required) - **kind: "note"** - `block_version` (integer, > 0; required) - `content` (object; required) - `schema_version` ("studio-ast-2"; required) - `text` (string, max 20000 chars; required) - `type` ("note"; required) - `generation_profile_override` (object | null; required): Partial saved profile. New document mutations validate the effective inherited provider/model and options against StudioGenerationProfileRequest. The read schema stays permissive for historical profiles. Join options with speaker context; an override is not a standalone full profile. - `controls` (string, one of `strict`, `best_effort`; optional) - `fallback` (string, one of `strict`, `best_effort`; optional) - `model` (string, 1–200 chars; optional) - `options` (map of boolean | string | integer | number | array of StudioAstJsonValue | map of StudioAstJsonValue | null; optional) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; optional) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("note"; required) - `placement` (any | null; required) - `speaker_id` (any | null; required) - **kind: "sfx"** - `block_version` (integer, > 0; required) - `content` (object; required) - `duration_ms` (integer, 100–3900000; required): Generated sound effects (no source) allow up to 300000 ms; a pinned project sound allows up to 3900000 ms. - `prompt` (string, 1–4000 chars; required) - `schema_version` ("studio-ast-2"; required) - `seed` (integer | null, ≥ -9007199254740991; required) - `type` ("sfx"; required) - `source` (object; optional): Present only when the SFX block plays an uploaded project sound; seed is then null. - `kind` ("project_sound"; required) - `sound_effect_id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `sound_effect_revision_id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `generation_profile_override` (object | null; required): Partial saved profile. New document mutations validate the effective inherited provider/model and options against StudioGenerationProfileRequest. The read schema stays permissive for historical profiles. Join options with speaker context; an override is not a standalone full profile. - `controls` (string, one of `strict`, `best_effort`; optional) - `fallback` (string, one of `strict`, `best_effort`; optional) - `model` (string, 1–200 chars; optional) - `options` (map of boolean | string | integer | number | array of StudioAstJsonValue | map of StudioAstJsonValue | null; optional) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; optional) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("sfx"; required) - `placement` (object; required) - `anchor` (object; required) - One of: - **kind: "block"** - `block_id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `fraction` (number | null, 0–1; required) - `kind` ("block"; required) - `position` (string, one of `start`, `fraction`, `end`, `after`; required) - **kind: "section"** - `kind` ("section"; required) - `position` (string, one of `start`, `end`; required) - **kind: "unplaced"** - `kind` ("unplaced"; required) - `fade_in_ms` (integer, 0–300000; required) - `fade_out_ms` (integer, 0–300000; required) - `gain_db` (number, -60–24; required) - `offset_ms` (integer, -300000–300000; required) - `speaker_id` (any | null; required) - **kind: "delete_block"** - `kind` ("delete_block"; required) - `block_id` (string, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - **kind: "move_block"** - `kind` ("move_block"; required) - `block_id` (string, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - `to_index` (integer, ≥ 0; required) - **kind: "create_sfx"**: For an uploaded project sound, use content.source.kind=project_sound, seed=null, and a section or block placement anchor. - `kind` ("create_sfx"; required) - `block_id` (string, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - `content` (object; required) - `duration_ms` (integer, 100–3900000; required): Generated sound effects (no source) allow up to 300000 ms; a pinned project sound allows up to 3900000 ms. - `prompt` (string, 1–4000 chars; required) - `schema_version` ("studio-ast-2"; required) - `seed` (integer | null, ≥ -9007199254740991; required) - `type` ("sfx"; required) - `source` (object; optional): Present only when the SFX block plays an uploaded project sound; seed is then null. - `kind` ("project_sound"; required) - `sound_effect_id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `sound_effect_revision_id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `placement` (object; required) - `anchor` (object; required) - One of: - **kind: "block"** - `block_id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `fraction` (number | null, 0–1; required) - `kind` ("block"; required) - `position` (string, one of `start`, `fraction`, `end`, `after`; required) - **kind: "section"** - `kind` ("section"; required) - `position` (string, one of `start`, `end`; required) - **kind: "unplaced"** - `kind` ("unplaced"; required) - `fade_in_ms` (integer, 0–300000; required) - `fade_out_ms` (integer, 0–300000; required) - `gain_db` (number, -60–24; required) - `offset_ms` (integer, -300000–300000; required) - **kind: "update_sfx"** - `kind` ("update_sfx"; required) - `block_id` (string, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - `content` (object; required) - `duration_ms` (integer, 100–3900000; required): Generated sound effects (no source) allow up to 300000 ms; a pinned project sound allows up to 3900000 ms. - `prompt` (string, 1–4000 chars; required) - `schema_version` ("studio-ast-2"; required) - `seed` (integer | null, ≥ -9007199254740991; required) - `type` ("sfx"; required) - `source` (object; optional): Present only when the SFX block plays an uploaded project sound; seed is then null. - `kind` ("project_sound"; required) - `sound_effect_id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `sound_effect_revision_id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `placement` (object; required) - `anchor` (object; required) - One of: - **kind: "block"** - `block_id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `fraction` (number | null, 0–1; required) - `kind` ("block"; required) - `position` (string, one of `start`, `fraction`, `end`, `after`; required) - **kind: "section"** - `kind` ("section"; required) - `position` (string, one of `start`, `end`; required) - **kind: "unplaced"** - `kind` ("unplaced"; required) - `fade_in_ms` (integer, 0–300000; required) - `fade_out_ms` (integer, 0–300000; required) - `gain_db` (number, -60–24; required) - `offset_ms` (integer, -300000–300000; required) - **kind: "split_blocks"** - `kind` ("split_blocks"; required) - `targets` (array of object, 1–2 items; required) - `block_id` (string, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - `split_offsets_utf16` (array of integer, 1–2 items; required) - `new_block_ids` (array of string, 1–2 items; required) - `annotation_fragment_ids` (map of array of string; required) - **kind: "merge_blocks"**: Left and right block IDs must differ. - `kind` ("merge_blocks"; required) - `left_block_id` (string, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - `right_block_id` (string, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - `expected_left_text_length_utf16` (integer, ≥ 0; required) - **kind: "upsert_annotation"** - `kind` ("upsert_annotation"; required) - `block_id` (string, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - `annotation` (object; required) - One of: - **kind: "exclusion"** - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("exclusion"; required) - `reason` (string, one of `speaker_label`, `stage_direction`, `metadata`, `user`; required) - `start_utf16` (integer, ≥ 0; required) - **kind: "pronunciation"** - `alphabet` (string, one of `ipa`, `x-sampa`, `cmu`, `alias`; required) - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("pronunciation"; required) - `start_utf16` (integer, ≥ 0; required) - `value` (string, 1–500 chars; required) - **kind: "extension"** - `end_utf16` (integer, ≥ 0; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("extension"; required) - `name` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `namespace` (string, pattern ^[a-z][a-z0-9_.-]{0,63}$; required) - `payload` (boolean | string | integer | number | array of boolean | string | integer | number | array of StudioAstJsonValue | map of StudioAstJsonValue | null | map of boolean | string | integer | number | array of StudioAstJsonValue | map of StudioAstJsonValue | null | null; required) - `start_utf16` (integer, ≥ 0; required) - `version` (integer, > 0; required) - **kind: "remove_annotation"** - `kind` ("remove_annotation"; required) - `block_id` (string, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - `annotation_id` (string, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - **kind: "upsert_point"** - `kind` ("upsert_point"; required) - `block_id` (string, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - `point` (object; required) - One of: - **kind: "timed_silence"** - `duration_ms` (integer, 1–300000; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("timed_silence"; required) - `offset_utf16` (integer, ≥ 0; required) - **kind: "extension"** - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("extension"; required) - `name` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `namespace` (string, pattern ^[a-z][a-z0-9_.-]{0,63}$; required) - `offset_utf16` (integer, ≥ 0; required) - `payload` (boolean | string | integer | number | array of boolean | string | integer | number | array of StudioAstJsonValue | map of StudioAstJsonValue | null | map of boolean | string | integer | number | array of StudioAstJsonValue | map of StudioAstJsonValue | null | null; required) - `version` (integer, > 0; required) - **kind: "remove_point"** - `kind` ("remove_point"; required) - `block_id` (string, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - `point_id` (string, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - **kind: "upsert_delivery_control_set"** - `kind` ("upsert_delivery_control_set"; required) - `block_id` (string, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - `set` (object; required): Provider-specific delivery controls for one provider/model. The format fixes the provider, model, and which setting and directive kinds are allowed (StudioDeliveryControlSetSchema in libs/studio/ast/v2/delivery-controls.ts). source_fingerprint and source_utf16_length are non-null exactly when directives or provider_text_override are present. - `capability_revision` (string, 1–200 chars; required) - `directives` (array of object, max 256 items; required) - One of: - **kind: "inworld.turn_instruction"** - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `instruction` (string, 1–4000 chars; required) - `kind` ("inworld.turn_instruction"; required) - `scope` (object; optional) - One of: - **kind: "block"** - `kind` ("block"; required) - **kind: "point"** - `kind` ("point"; required) - `offset_utf16` (integer, ≥ 0; required) - `ordinal` (integer, 0–255; required) - **kind: "inworld.nonverbal"** - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("inworld.nonverbal"; required) - `offset_utf16` (integer, ≥ 0; required) - `ordinal` (integer, 0–255; required) - `value` (string, one of `laugh`, `breathe`, `clear_throat`, `sigh`, `cough`, `yawn`; required) - **kind: "fish.inline_instruction"** - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `instruction` (string, 1–4000 chars; required) - `kind` ("fish.inline_instruction"; required) - `offset_utf16` (integer, ≥ 0; required) - `ordinal` (integer, 0–255; required) - **kind: "cartesia.emotion"** - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("cartesia.emotion"; required) - `scope` (object; required) - One of: - **kind: "block"** - `kind` ("block"; required) - **kind: "point"** - `kind` ("point"; required) - `offset_utf16` (integer, ≥ 0; required) - `ordinal` (integer, 0–255; required) - `value` (string, one of `neutral`, `happy`, `excited`, `enthusiastic`, `elated`, `euphoric`, `triumphant`, `amazed`, `surprised`, `flirtatious`, `curious`, `content`, `peaceful`, `serene`, `calm`, `grateful`, `affectionate`, `trust`, `sympathetic`, `anticipation`, `mysterious`, `angry`, `mad`, `outraged`, `frustrated`, `agitated`, `threatened`, `disgusted`, `contempt`, `envious`, `sarcastic`, `ironic`, `sad`, `dejected`, `melancholic`, `disappointed`, `hurt`, `guilty`, `bored`, `tired`, `rejected`, `nostalgic`, `wistful`, `apologetic`, `hesitant`, `insecure`, `confused`, `resigned`, `anxious`, `panicked`, `alarmed`, `scared`, `proud`, `confident`, `distant`, `skeptical`, `contemplative`, `determined`; required) - **kind: "cartesia.laughter"** - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("cartesia.laughter"; required) - `offset_utf16` (integer, ≥ 0; required) - `ordinal` (integer, 0–255; required) - **kind: "openai.instructions"** - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `instruction` (string, 1–4000 chars; required) - `kind` ("openai.instructions"; required) - **kind: "chatterbox.nonverbal"** - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("chatterbox.nonverbal"; required) - `offset_utf16` (integer, ≥ 0; required) - `ordinal` (integer, 0–255; required) - `value` (string, one of `laugh`, `chuckle`, `sigh`, `cough`, `sniffle`, `groan`, `yawn`, `gasp`; required) - **kind: "elevenlabs.audio_tag"** - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("elevenlabs.audio_tag"; required) - `instruction` (string, 1–200 chars; required): Trimmed audio-tag text without square brackets or control characters. - `scope` (object; required) - One of: - **kind: "block"** - `kind` ("block"; required) - **kind: "point"** - `kind` ("point"; required) - `offset_utf16` (integer, ≥ 0; required) - `ordinal` (integer, 0–255; required) - `format` (string, one of `eleven-v4-1`, `inworld-tts2-1`, `inworld-tts2-experimental-1`, `inworld-tts15-max-1`, `inworld-tts15-mini-1`, `fish-s2-inline-1`, `fish-s2.1-inline-1`, `cartesia-sonic36-1`, `cartesia-sonic35-1`, `openai-gpt4o-mini-tts-1`, `openai-tts1-hd-1`, `openai-tts1-1`, `smallest-lightning31-1`, `chatterbox-classic-1`, `zyphra-zonos2-cloud-1`, `f5tts-v1-1`, `misotts-8b-1`; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `model` (string, 1–200 chars; required) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `provider_text_override` (string, 1–65536 chars, pattern [\s\S]*\S[\s\S]*; optional) - `schema_version` (string, one of `studio-delivery-control-set-1`, `studio-delivery-control-set-2`; required) - `settings` (array of object, max 32 items; required) - One of: - **kind: "inworld.speaking_rate"** - `kind` ("inworld.speaking_rate"; required) - `value` (number, 0.5–1.5; required) - **kind: "inworld.delivery_mode"** - `kind` ("inworld.delivery_mode"; required) - `value` (string, one of `stable`, `balanced`, `creative`; required) - **kind: "fish.speed"** - `kind` ("fish.speed"; required) - `value` (number, 0.5–2; required) - **kind: "fish.volume"** - `kind` ("fish.volume"; required) - `value` (number, -20–20; required) - **kind: "openai.speed"** - `kind` ("openai.speed"; required) - `value` (number, 0.25–4; required) - **kind: "smallest.speed"** - `kind` ("smallest.speed"; required) - `value` (number, 0.5–2; required) - **kind: "chatterbox.exaggeration"** - `kind` ("chatterbox.exaggeration"; required) - `value` (number, 0–1; required) - **kind: "chatterbox.cfg"** - `kind` ("chatterbox.cfg"; required) - `value` (number, 0–1; required) - **kind: "zyphra.mode"** - `kind` ("zyphra.mode"; required) - `value` (string, one of `stable`, `expressive`; required) - **kind: "zyphra.speaking_rate_bucket"** - `kind` ("zyphra.speaking_rate_bucket"; required) - `value` (integer, 0–7; required) - **kind: "f5tts.speed"** - `kind` ("f5tts.speed"; required) - `value` (number, 0.3–2; required) - `source_fingerprint` (string | null, pattern ^[0-9a-f]{64}$; required) - `source_utf16_length` (integer | null, ≥ 0; required) - **kind: "remove_delivery_control_set"** - `kind` ("remove_delivery_control_set"; required) - `block_id` (string, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `model` (string, 1–200 chars; required) - **kind: "set_block_attribution"**: Target block IDs must be unique. - `kind` ("set_block_attribution"; required) - `targets` (array of object, 1–1000 items; required) - `block_id` (string, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - `block_kind` (string, one of `speech`, `narration`, `unparsed`, `excluded`; required) - `speaker_id` (string | null, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - **kind: "create_speaker"**: The speaker name must be trimmed. speaker_version is 1 for a new speaker, or restore_from_speaker_version+1. A non-null casting_default_key must equal the normalized speaker name and requires voice_id. - `kind` ("create_speaker"; required) - `speaker` (object; required): A speaker written by a document command. New full profiles use the strict provider/model request contract; saved-document reads retain their historical profile shape. - `generation_profile` (object | null; required): Saved profile read shape, including historical options. For new full-profile requests use StudioGenerationProfileRequest; its provider/model branches list accepted option keys. Generation eligibility and actual audio readiness are separate reads. - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `model` (string, 1–200 chars; required) - `fallback` (string, one of `strict`, `best_effort`; required) - `controls` (string, one of `strict`, `best_effort`; required) - `options` (object; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `name` (string, 1–200 chars; required) - `speaker_version` (integer, > 0; required) - `voice_id` (string | null, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `restore_from_speaker_version` (integer | null, ≥ 1; required) - `casting_default_key` (string | null, 1–200 chars; required) - **kind: "update_speaker"**: The name must be trimmed. - `kind` ("update_speaker"; required) - `speaker_id` (string, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - `name` (string, 1–200 chars; required) - **kind: "delete_speaker"** - `kind` ("delete_speaker"; required) - `speaker_id` (string, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - **kind: "set_speaker_casting"**: A non-null casting_default_key requires a voice_id. The effective profile is validated in speaker context. - `kind` ("set_speaker_casting"; required) - `speaker_id` (string, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - `voice_id` (string | null, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - `generation_profile` (object | null; required): Request-only provider/model-specific options for new full profiles. Catalog-only compatibility models can be saved but are unavailable for generation; check generation-availability. Historical document profiles remain permissive. Partial overrides are validated after inheriting their effective provider, model, and options. Stable Audio is for generated SFX, not speaker casting. - `casting_default_key` (string | null, 1–200 chars; required) - **kind: "set_generation_profile_override"**: Target block IDs must be unique; effective options are validated against the inherited provider/model. - `kind` ("set_generation_profile_override"; required) - `targets` (array of object, 1–1000 items; required) - `block_id` (string, pattern ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$; required) - `generation_profile_override` (object | null; required): Partial saved profile. New document mutations validate the effective inherited provider/model and options against StudioGenerationProfileRequest. The read schema stays permissive for historical profiles. Join options with speaker context; an override is not a standalone full profile. - `controls` (string, one of `strict`, `best_effort`; optional) - `fallback` (string, one of `strict`, `best_effort`; optional) - `model` (string, 1–200 chars; optional) - `options` (map of object | null; optional) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; optional) - **kind: "apply_source_document"**: Replaces the source document under the save request's version fences. - `kind` ("apply_source_document"; required) - `document` (object; required): A replacement source document written by a command. Speaker profiles use the strict new-write request contract. - `blocks` (array of object, max 100000 items; required) - One of: - **kind: "speech"** - `block_version` (integer, > 0; required) - `content` (object; required) - `annotations` (array of StudioAstTextAnnotation; required) - `delivery_control_sets` (array of StudioAstDeliveryControlSet, max 32 items; required) - `points` (array of StudioAstPointNode; required) - `schema_version` ("studio-ast-2"; required) - `text` (string, max 65536 chars; required): Exact block text; at most 65,536 UTF-16 code units. Annotation, point, and delivery offsets are UTF-16 offsets into this string. - `type` ("text"; required) - `generation_profile_override` (object | null; required): Partial saved profile. New document mutations validate the effective inherited provider/model and options against StudioGenerationProfileRequest. The read schema stays permissive for historical profiles. Join options with speaker context; an override is not a standalone full profile. - `controls` (string, one of `strict`, `best_effort`; optional) - `fallback` (string, one of `strict`, `best_effort`; optional) - `model` (string, 1–200 chars; optional) - `options` (map of StudioAstJsonValue; optional) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; optional) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("speech"; required) - `placement` (any | null; required) - `speaker_id` (string | null, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - **kind: "narration"** - `block_version` (integer, > 0; required) - `content` (object; required) - `annotations` (array of StudioAstTextAnnotation; required) - `delivery_control_sets` (array of StudioAstDeliveryControlSet, max 32 items; required) - `points` (array of StudioAstPointNode; required) - `schema_version` ("studio-ast-2"; required) - `text` (string, max 65536 chars; required): Exact block text; at most 65,536 UTF-16 code units. Annotation, point, and delivery offsets are UTF-16 offsets into this string. - `type` ("text"; required) - `generation_profile_override` (object | null; required): Partial saved profile. New document mutations validate the effective inherited provider/model and options against StudioGenerationProfileRequest. The read schema stays permissive for historical profiles. Join options with speaker context; an override is not a standalone full profile. - `controls` (string, one of `strict`, `best_effort`; optional) - `fallback` (string, one of `strict`, `best_effort`; optional) - `model` (string, 1–200 chars; optional) - `options` (map of StudioAstJsonValue; optional) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; optional) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("narration"; required) - `placement` (any | null; required) - `speaker_id` (string | null, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - **kind: "unparsed"** - `block_version` (integer, > 0; required) - `content` (object; required) - `annotations` (array of StudioAstTextAnnotation; required) - `delivery_control_sets` (array of StudioAstDeliveryControlSet, max 32 items; required) - `points` (array of StudioAstPointNode; required) - `schema_version` ("studio-ast-2"; required) - `text` (string, max 65536 chars; required): Exact block text; at most 65,536 UTF-16 code units. Annotation, point, and delivery offsets are UTF-16 offsets into this string. - `type` ("text"; required) - `generation_profile_override` (object | null; required): Partial saved profile. New document mutations validate the effective inherited provider/model and options against StudioGenerationProfileRequest. The read schema stays permissive for historical profiles. Join options with speaker context; an override is not a standalone full profile. - `controls` (string, one of `strict`, `best_effort`; optional) - `fallback` (string, one of `strict`, `best_effort`; optional) - `model` (string, 1–200 chars; optional) - `options` (map of StudioAstJsonValue; optional) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; optional) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("unparsed"; required) - `placement` (any | null; required) - `speaker_id` (any | null; required) - **kind: "excluded"** - `block_version` (integer, > 0; required) - `content` (object; required) - `annotations` (array of StudioAstTextAnnotation; required) - `delivery_control_sets` (array of StudioAstDeliveryControlSet, max 32 items; required) - `points` (array of StudioAstPointNode; required) - `schema_version` ("studio-ast-2"; required) - `text` (string, max 65536 chars; required): Exact block text; at most 65,536 UTF-16 code units. Annotation, point, and delivery offsets are UTF-16 offsets into this string. - `type` ("text"; required) - `generation_profile_override` (object | null; required): Partial saved profile. New document mutations validate the effective inherited provider/model and options against StudioGenerationProfileRequest. The read schema stays permissive for historical profiles. Join options with speaker context; an override is not a standalone full profile. - `controls` (string, one of `strict`, `best_effort`; optional) - `fallback` (string, one of `strict`, `best_effort`; optional) - `model` (string, 1–200 chars; optional) - `options` (map of StudioAstJsonValue; optional) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; optional) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("excluded"; required) - `placement` (any | null; required) - `speaker_id` (any | null; required) - **kind: "note"** - `block_version` (integer, > 0; required) - `content` (object; required) - `schema_version` ("studio-ast-2"; required) - `text` (string, max 20000 chars; required) - `type` ("note"; required) - `generation_profile_override` (object | null; required): Partial saved profile. New document mutations validate the effective inherited provider/model and options against StudioGenerationProfileRequest. The read schema stays permissive for historical profiles. Join options with speaker context; an override is not a standalone full profile. - `controls` (string, one of `strict`, `best_effort`; optional) - `fallback` (string, one of `strict`, `best_effort`; optional) - `model` (string, 1–200 chars; optional) - `options` (map of StudioAstJsonValue; optional) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; optional) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("note"; required) - `placement` (any | null; required) - `speaker_id` (any | null; required) - **kind: "sfx"** - `block_version` (integer, > 0; required) - `content` (object; required) - `duration_ms` (integer, 100–3900000; required): Generated sound effects (no source) allow up to 300000 ms; a pinned project sound allows up to 3900000 ms. - `prompt` (string, 1–4000 chars; required) - `schema_version` ("studio-ast-2"; required) - `seed` (integer | null, ≥ -9007199254740991; required) - `type` ("sfx"; required) - `source` (object; optional): Present only when the SFX block plays an uploaded project sound; seed is then null. - `kind` (object; required) - `sound_effect_id` (object; required) - `sound_effect_revision_id` (object; required) - `generation_profile_override` (object | null; required): Partial saved profile. New document mutations validate the effective inherited provider/model and options against StudioGenerationProfileRequest. The read schema stays permissive for historical profiles. Join options with speaker context; an override is not a standalone full profile. - `controls` (string, one of `strict`, `best_effort`; optional) - `fallback` (string, one of `strict`, `best_effort`; optional) - `model` (string, 1–200 chars; optional) - `options` (map of StudioAstJsonValue; optional) - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; optional) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `kind` ("sfx"; required) - `placement` (object; required) - `anchor` (object; required) - One of: - **kind: "block"** - `block_id` (object; required) - `fraction` (object; required) - `kind` (object; required) - `position` (object; required) - **kind: "section"** - `kind` (object; required) - `position` (object; required) - **kind: "unplaced"** - `kind` (object; required) - `fade_in_ms` (integer, 0–300000; required) - `fade_out_ms` (integer, 0–300000; required) - `gain_db` (number, -60–24; required) - `offset_ms` (integer, -300000–300000; required) - `speaker_id` (any | null; required) - `different_speaker_gap_ms` (integer, 0–1000; required): Silence in milliseconds inserted between adjacent spoken blocks with different speakers. - `document_version` (integer, > 0; required) - `sfx_enabled` (boolean; required): Whether authored section sound effects are audible in playback and included in rendered exports. Legacy sections without a stored value report true. - `same_speaker_gap_ms` (integer, 0–1000; required): Silence in milliseconds inserted between adjacent spoken blocks with the same speaker. - `schema_version` ("studio-ast-2"; required) - `section_id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required): Canonical lowercase UUID of the section. - `speaker_registry_version` (integer, > 0; required): Project-wide speaker registry version the speakers list was read at. - `speakers` (array of object; required): Every active speaker in the project registry, not only speakers referenced by this section. - `generation_profile` (object | null; required): Saved profile read shape, including historical options. For new full-profile requests use StudioGenerationProfileRequest; its provider/model branches list accepted option keys. Generation eligibility and actual audio readiness are separate reads. - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `model` (string, 1–200 chars; required) - `fallback` (string, one of `strict`, `best_effort`; required) - `controls` (string, one of `strict`, `best_effort`; required) - `options` (object; required) - `id` (string, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `name` (string, 1–200 chars; required) - `speaker_version` (integer, > 0; required) - `voice_id` (string | null, pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$; required) - `title` (string, max 500 chars; required) #### Response 200 Authoritative document after commit or idempotent replay. Fields inside `data`: - `actor_user_id` (string, uuid; required): Authenticated caller; clients partition local recovery state by this ID. - `project_id` (string, uuid; required) - `document` (object; required): The canonical structured script of one section (studio-ast-2): its versions, speakers, and ordered blocks. See [Get a section document](https://lyricwinter.com/docs/api/documents#get-studio-document) for its fields. - `source_draft` (object | null; required): The caller's saved, uncommitted Advanced Source draft for this section, or null. - `source_text` (string; required) - `base_document_version` (integer, ≥ 1; required) - `base_speaker_registry_version` (integer, ≥ 1; required) - `base_speaker_versions` (map of integer; required): Speaker versions keyed by speaker ID when the draft began. - `diagnostics` (array of object, max 1000 items; required) - `code` (string, one of `INVALID_SOURCE`, `STALE_SOURCE`; required) - `message` (string, 1–2000 chars; required) - `updated_at` (string, date-time; required) Errors: `400`, `401`, `403`, `404`, `409`, `500` return the standard error envelope. #### Example request ```bash curl -X PATCH "https://lyricwinter.com/api/v1/studio/sections/$SECTION_ID/document" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schema_version": 1, "actor_session_id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10", "client_mutation_id": "c9f0f895-fb98-4b91-9e3f-1d2a6b7c8d20", "base_document_version": 1, "base_speaker_registry_version": 1, "target_block_versions": {}, "target_speaker_versions": {}, "commands": [ { "kind": "set_title", "title": "Chapter One" } ] }' ``` ### Import a speaker-labeled script `POST /studio/sections/{sectionId}/script` Scope: `studio:write` · Retries: reuse the same `client_mutation_id` · Operation ID: `importStudioScript` Turn `SPEAKER: [optional direction] spoken text` lines into a Studio document without building an AST. Map each speaker label to a voice and generation profile. A separate `(silence 4s)` line adds exact silence after the preceding spoken line. Unsupported lines or directions fail rather than disappearing. This replaces the section script; it does not generate audio. Send the current document and speaker-registry versions. Reusing an existing speaker name updates that project-wide speaker's voice and profile. Retry uncertain responses with the same mutation ID and body; a receipt that cannot be verified returns `409` without importing again. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `sectionId` | string, uuid | Yes | | #### Request body (application/json, required) - `schema_version` (1; required) - `actor_session_id` (string, uuid; required) - `client_mutation_id` (string, uuid; required) - `base_document_version` (integer, ≥ 1; required) - `base_speaker_registry_version` (integer, ≥ 1; required) - `script` (string, min 1 chars; required): One SPEAKER: [optional direction] spoken text per nonblank line. A separate (silence 4s) or (silence 4000ms) line adds 1-300 seconds of silence after the preceding spoken line. Labels match speaker-map keys exactly. Unsupported lines are rejected. - `speakers` (map of object; required): Exact speaker-label keys used in the script, each with an explicit voice and generation profile. Existing project speakers with the same name are reused. - `voice_id` (string | null, uuid; required) - `generation_profile` (object; required): Request-only provider/model-specific options for new full profiles. Catalog-only compatibility models can be saved but are unavailable for generation; check generation-availability. Historical document profiles remain permissive. Partial overrides are validated after inheriting their effective provider, model, and options. Stable Audio is for generated SFX, not speaker casting. #### Response 200 Canonical Studio document after the import. Fields inside `data`: - `actor_user_id` (string, uuid; required): Authenticated caller; clients partition local recovery state by this ID. - `project_id` (string, uuid; required) - `document` (object; required): The canonical structured script of one section (studio-ast-2): its versions, speakers, and ordered blocks. See [Get a section document](https://lyricwinter.com/docs/api/documents#get-studio-document) for its fields. - `source_draft` (object | null; required): The caller's saved, uncommitted Advanced Source draft for this section, or null. - `source_text` (string; required) - `base_document_version` (integer, ≥ 1; required) - `base_speaker_registry_version` (integer, ≥ 1; required) - `base_speaker_versions` (map of integer; required): Speaker versions keyed by speaker ID when the draft began. - `diagnostics` (array of object, max 1000 items; required) - `code` (string, one of `INVALID_SOURCE`, `STALE_SOURCE`; required) - `message` (string, 1–2000 chars; required) - `updated_at` (string, date-time; required) Errors: `400`, `401`, `403`, `404`, `409`, `500` return the standard error envelope. #### Example request ```bash curl -X POST "https://lyricwinter.com/api/v1/studio/sections/$SECTION_ID/script" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schema_version": 1, "actor_session_id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10", "client_mutation_id": "c9f0f895-fb98-4b91-9e3f-1d2a6b7c8d20", "base_document_version": 1, "base_speaker_registry_version": 1, "script": "string", "speakers": {} }' ``` ### List project speakers `GET /studio/projects/{projectId}/speakers` Scope: `studio:read` · Operation ID: `getStudioProjectSpeakerUsage` Returns the project's speakers with how many lines and sections each one appears in, plus the current `speaker_registry_version`. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | #### Response 200 Project-wide character usage and current character registry authority. Fields inside `data`: - `project_id` (string, uuid; required) - `speaker_registry_version` (integer, ≥ 1; required) - `speakers` (array of object, max 10000 items; required): Every active project speaker, including unused speakers with zero counts. - `speaker_id` (string, uuid; required) - `line_count` (integer, ≥ 0; required): Active speech and narration blocks assigned to the speaker across active sections. - `block_override_count` (integer, ≥ 0; required): Of those blocks, how many carry a block-level generation profile override. - `section_count` (integer, ≥ 0; required): Active sections containing at least one of those blocks. Errors: `400`, `401`, `403`, `404`, `500` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/speakers" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Assign a speaker's voice and model `PATCH /studio/projects/{projectId}/speakers/{speakerId}/casting` Scope: `studio:write` · Retries: reuse the same `client_mutation_id` · Operation ID: `updateStudioProjectSpeakerCasting` Updates one speaker across its project with a voice and full generation profile, or clears both together. Send current speaker and speaker-registry versions plus session/mutation IDs and a current section ID; the result contains the casting mutation and the current section document. Provider options are typed by provider/model. ElevenLabs v4 accepts optional stability (0-1) and integer seed (1-4294967295); empty options retain defaults. Seed is best-effort, not guaranteed deterministic. Speed and similarity_boost are not exposed by the connected transport. Unsupported keys fail before saving or generation. Author delivery directions through control sets. Partial block overrides inherit speaker settings and are checked against the resulting effective profile. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | | `speakerId` | string, uuid | Yes | | #### Request body (application/json, required) - `schema_version` (1; required) - `actor_session_id` (string, uuid; required) - `client_mutation_id` (string, uuid; required) - `current_section_id` (string, uuid; required) - `base_speaker_registry_version` (integer, ≥ 1; required) - `speaker_version` (integer, ≥ 1; required) - `voice_id` (string | null, uuid; required) - `generation_profile` (object | null; required): Request-only provider/model-specific options for new full profiles. Catalog-only compatibility models can be saved but are unavailable for generation; check generation-availability. Historical document profiles remain permissive. Partial overrides are validated after inheriting their effective provider, model, and options. Stable Audio is for generated SFX, not speaker casting. - `casting_default_key` (string | null, 1–200 chars; required) #### Response 200 Project casting changed and the current section returned at the committed speaker authority. Fields inside `data`: - `mutation` (object; required) - `mutation_id` (string, uuid; required) - `reverted_mutation_id` (string | null, uuid; required) - `project_id` (string, uuid; required) - `speaker_id` (string, uuid; required) - `speaker_registry_version` (integer, ≥ 1; required) - `speaker_version` (integer, ≥ 1; required) - `affected_block_count` (integer, ≥ 0; required) - `affected_section_count` (integer, ≥ 0; required) - `idempotent_replay` (boolean; required) - `document` (object; required) - `actor_user_id` (string, uuid; required): Authenticated caller; clients partition local recovery state by this ID. - `project_id` (string, uuid; required) - `document` (object; required): The canonical structured script of one section (studio-ast-2): its versions, speakers, and ordered blocks. See [Get a section document](https://lyricwinter.com/docs/api/documents#get-studio-document) for its fields. - `source_draft` (object | null; required): The caller's saved, uncommitted Advanced Source draft for this section, or null. - `source_text` (string; required) - `base_document_version` (integer, ≥ 1; required) - `base_speaker_registry_version` (integer, ≥ 1; required) - `base_speaker_versions` (map of integer; required): Speaker versions keyed by speaker ID when the draft began. - `diagnostics` (array of object, max 1000 items; required) - `code` (string, one of `INVALID_SOURCE`, `STALE_SOURCE`; required) - `message` (string, 1–2000 chars; required) - `updated_at` (string, date-time; required) Errors: `400`, `401`, `403`, `404`, `409`, `500` return the standard error envelope. #### Example request ```bash curl -X PATCH "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/speakers/$SPEAKER_ID/casting" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schema_version": 1, "actor_session_id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10", "client_mutation_id": "c9f0f895-fb98-4b91-9e3f-1d2a6b7c8d20", "current_section_id": "45c48cce-2e2d-4fbd-8a1c-5e6f7a8b9c30", "base_speaker_registry_version": 1, "speaker_version": 1, "voice_id": "d3d94468-02a4-4c5e-b6f7-0a1b2c3d4e40", "generation_profile": {}, "casting_default_key": "string" }' ``` --- Source: https://lyricwinter.com/docs/api/workflows.md --- title: "Workflows API" description: "Workflows are durable, asynchronous jobs. A prepare workflow structures text into narration and dialogue; a Create Audio workflow casts voices and generates multi-voice audio. Poll a workflow until it reaches a terminal status. A provider-account limit is reported as a failed step with `STUDIO_WORKFLOW_PROVIDER_QUOTA_EXCEEDED`; generation-batch targets use `PROVIDER_QUOTA_EXCEEDED`. Completed audio remains available. For a shared provider limit, contact us to restore provider access before explicitly resuming. When the failure message says the account's monthly ElevenLabs voice preparation allowance is used up, choose a cached ElevenLabs voice or another engine, or wait for the allowance to reset on the first of the month (UTC). Do not automatically resubmit quota failures." canonical_url: https://lyricwinter.com/docs/api/workflows markdown_url: https://lyricwinter.com/docs/api/workflows.md last_updated: 2026-10-04 status: beta --- # LyricWinter API reference: Workflows > Workflows are durable, asynchronous jobs. A prepare workflow structures text into narration and dialogue; a Create Audio workflow casts voices and generates multi-voice audio. Poll a workflow until it reaches a terminal status. A provider-account limit is reported as a failed step with `STUDIO_WORKFLOW_PROVIDER_QUOTA_EXCEEDED`; generation-batch targets use `PROVIDER_QUOTA_EXCEEDED`. Completed audio remains available. For a shared provider limit, contact us to restore provider access before explicitly resuming. When the failure message says the account's monthly ElevenLabs voice preparation allowance is used up, choose a cached ElevenLabs voice or another engine, or wait for the allowance to reset on the first of the month (UTC). Do not automatically resubmit quota failures. The LyricWinter API is in beta. Base URL: `https://lyricwinter.com/api/v1`. Authenticate every request with `Authorization: Bearer lw_...`. Successful responses are wrapped as `{ "data": ..., "request_id": "req_..." }`. Machine-readable contract: https://lyricwinter.com/api/v1/openapi.json. ## Endpoints - [Start a workflow](https://lyricwinter.com/docs/api/workflows#create-studio-workflow): `POST /studio/projects/{projectId}/workflows` - [Get the latest project workflow](https://lyricwinter.com/docs/api/workflows#get-latest-studio-workflow): `GET /studio/projects/{projectId}/workflows` - [Get a workflow](https://lyricwinter.com/docs/api/workflows#get-studio-workflow): `GET /studio/workflows/{workflowRunId}` - [Get workflow audio progress](https://lyricwinter.com/docs/api/workflows#get-studio-workflow-progress): `GET /studio/workflows/{workflowRunId}/progress` - [Cancel a workflow](https://lyricwinter.com/docs/api/workflows#cancel-studio-workflow): `POST /studio/workflows/{workflowRunId}/cancel` - [Resume a failed workflow](https://lyricwinter.com/docs/api/workflows#resume-studio-workflow): `POST /studio/workflows/{workflowRunId}/resume` ### Start a workflow `POST /studio/projects/{projectId}/workflows` Scope: `studio:write` · Retries: reuse the same `client_mutation_id` · Operation ID: `createStudioWorkflow` Starts a `prepare_sections` workflow, which structures text into narration and dialogue, or a `create_audio` workflow, which prepares text, casts voices, directs performances, adds sound effects, and generates audio. Returns `202` with `data: { run, steps }`; poll the returned run ID until it reaches a terminal status. Replaying the same `client_mutation_id` returns the same workflow instead of starting another. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | #### Request body (application/json, required) - One of: - **schema_version: 1, workflow_kind: "prepare_sections"**: Start ordered section preparation. - `schema_version` (1; required) - `workflow_kind` ("prepare_sections"; required) - `actor_session_id` (string, uuid; required): Client session that issued the request. - `client_mutation_id` (string, uuid; required): Idempotency key; the workflow ID is derived from it. Reusing it for different content returns 409. - `section_ids` (array of string, 1–100 items, unique; required): Sections to prepare, in order; values must be unique. - **schema_version: 2, workflow_kind: "create_audio"**: Create Audio v2. Generated sound effects and automatic placement of linked project sounds are independent; when both run, project sounds are placed first. Section limit: 12 with both sound lanes enabled (the default), 14 with one, 16 with none. A target_scope requires exactly one section and both lanes set to false. - `schema_version` (2; required) - `workflow_kind` ("create_audio"; required) - `actor_session_id` (string, uuid; required): Client session that issued the request. - `client_mutation_id` (string, uuid; required): Idempotency key; the workflow ID is derived from it. Reusing it for different content returns 409. - `section_ids` (array of string, 1–16 items, unique; required): Sections to create audio for; values must be unique. - `include_generated_sfx` (boolean, default true; optional): Enables the generated-SFX authoring lane. - `place_project_sounds` (boolean, default true; optional): Enables automatic matching and placement from ready, accessible sounds explicitly linked to this project. - `direct_emotion` (boolean, default true; optional): Enables automatic emotion: a direct_emotion step adds provider delivery directions to spoken blocks that have none for their engine. Direction text sent to the provider is billed (ElevenLabs v4 audio tags). For pre-run estimates, clients add about 18 ElevenLabs v4 tokens per untagged ElevenLabs v4 block while this is on; the charge is the tokens actually sent. Omitted means true. - `target_scope` (object | null; optional): Optional focused source-block scope. Null or omitted targets the selected sections. Focused requests must set both optional sound lanes to false. - `kind` ("source_blocks"; required) - `blocks` (array of object, 1–250 items, unique; required): Focused source blocks; block_id values must be unique. - `block_id` (string, uuid; required) - `base_block_version` (integer, ≥ 1; required) - `text_length_utf16` (integer, 1–1000000; required) - `text_hash` (string, pattern ^[0-9a-f]{64}$; required) #### Response 202 Durable project workflow accepted and first advancement attempted. Replaying the same client_mutation_id with the same request returns the same workflow. Fields inside `data`: - `run` (object; required): Durable Studio project workflow run. - `id` (string, uuid; required): Workflow run ID. - `project_id` (string, uuid; required) - `requested_by_user_id` (string, uuid; required) - `payer_user_id` (string | null, uuid; required): User billed for Create Audio work; null for prepare_sections workflows. - `workflow_kind` (string, one of `prepare_sections`, `create_audio`; required) - `automation_profile` (string | null, one of `quick`, `balanced`, `directed`; required): Create Audio profile label from profile_snapshot.profile; null for prepare_sections. - `profile_snapshot` (object; required): Empty object for prepare_sections workflows; the frozen Create Audio options for create_audio workflows. - `status` (string, one of `queued`, `running`, `completed`, `partially_completed`, `failed`, `canceling`, `cancelled`; required): Workflow run status, recomputed from step statuses after every step transition. Non-terminal: queued (unfinished steps remain and none is active), running (at least one step is active), canceling (cancellation was requested and unfinished steps remain). Terminal (completed_at is set and the status no longer changes): completed (every step succeeded or was skipped), partially_completed (at least one failed or conflict step and at least one succeeded or skipped step), failed (failed or conflict steps and no succeeded or skipped step), cancelled (all steps finished after cancellation was requested). - `current_stage` (string | null, one of `preparing_sections`, `assigning_speakers`, `casting_voices`, `planning_emotion`, `directing_delivery`, `placing_project_sounds`, `directing_sfx`, `preparing_voices`, `generating_audio`; required): Stage of the next unfinished step while the run is queued, running, or canceling. Null once the run is terminal, or when the next unfinished step is render_section. - `step_count` (integer, 1–100; required) - `completed_step_count` (integer, 0–100; required): Steps in a terminal status (succeeded, skipped, failed, conflict, cancelled). - `progress` (object; required): Step counts by status. Currently contains integer counts pending, active, succeeded (succeeded or skipped), failed (failed or conflict), cancelled, completed (all terminal), and total; treat unknown keys as additive. - `pending` (integer, ≥ 0; optional) - `active` (integer, ≥ 0; optional) - `succeeded` (integer, ≥ 0; optional) - `failed` (integer, ≥ 0; optional) - `cancelled` (integer, ≥ 0; optional) - `completed` (integer, ≥ 0; optional) - `total` (integer, ≥ 0; optional) - `error_summary` (array of object, max 100 items; required): One entry per failed or conflict step in step order. Entries currently contain step_id, section_id, step_kind, code, and message; treat unknown keys as additive. - `step_id` (string, uuid; optional) - `section_id` (string, uuid; optional) - `step_kind` (string, one of `prepare_section`, `assign_speakers`, `cast_voices`, `direct_emotion`, `direct_section_delivery`, `place_project_sounds`, `direct_sfx`, `prepare_voices`, `generate_stale_audio`, `render_section`; optional): Kind of work a step performs. prepare_sections workflows contain only prepare_section steps. direct_section_delivery and render_section come from older Create Audio profiles (and successors resumed from them); new workflows do not plan them. - `code` (string | null; optional) - `message` (string | null; optional) - `cancel_requested_at` (string | null, date-time; required): When cancellation was first requested. - `cancel_acknowledged_at` (string | null, date-time; required): When the run reached cancelled. - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) - `started_at` (string | null, date-time; required): When execution first started; null before then. - `completed_at` (string | null, date-time; required): Set when the run reaches a terminal status (completed, partially_completed, failed, cancelled); null otherwise. - `resumes_workflow_run_id` (string | null, uuid; required): Predecessor run this run resumes; null for an original run. - `resumable` (boolean; required): True when status is failed or partially_completed and at least one step is failed or conflict. Only resumable runs can be resumed. - `steps` (array of object, 1–100 items; required): All steps in ascending step_index order. - `id` (string, uuid; required) - `workflow_run_id` (string, uuid; required) - `section_id` (string, uuid; required) - `step_key` (string, 1–200 chars; required): Stable key of the step within its workflow. - `step_index` (integer, 0–99; required): Zero-based position; steps are returned in ascending step_index order. - `step_kind` (string, one of `prepare_section`, `assign_speakers`, `cast_voices`, `direct_emotion`, `direct_section_delivery`, `place_project_sounds`, `direct_sfx`, `prepare_voices`, `generate_stale_audio`, `render_section`; required): Kind of work a step performs. prepare_sections workflows contain only prepare_section steps. direct_section_delivery and render_section come from older Create Audio profiles (and successors resumed from them); new workflows do not plan them. - `status` (string, one of `pending`, `active`, `succeeded`, `skipped`, `failed`, `conflict`, `cancelled`; required): Step status. pending and active are non-terminal; succeeded, skipped, failed, conflict, and cancelled are terminal. The run counts skipped as success and conflict as failure. conflict means the section document changed underneath the step. - `requested_document_version` (integer, ≥ 1; required): Section document version the step was planned against. - `execution_document_version` (integer | null, ≥ 1; required): Section document version the step executed against, when recorded. - `assistant_batch_id` (string | null, uuid; required): Child assistant batch started by an assistant-driven step. - `render_run_id` (string | null, uuid; required): Render run started by a render_section step. - `result_summary` (object; required): Step-specific result data, typed by the contract as an open JSON object. generate_stale_audio steps may carry the StudioWorkflowAudioResultSummary fields; steps cancelled before starting carry cancelled_before_start: true. - `issue_kind` (string | null, one of `document_changed`, `service_out_of_sync`, `assistant_failed`, `workflow_failed`; required): Set for failed and conflict steps; null for every other status. - `error_code` (string | null, 1–200 chars; required) - `error_message` (string | null, max 10000 chars; required) - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) - `started_at` (string | null, date-time; required) - `completed_at` (string | null, date-time; required) Errors: `400`, `401`, `403`, `404`, `409`, `500`, `503` return the standard error envelope. #### Example request ```bash curl -X POST "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/workflows" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schema_version": 1, "workflow_kind": "prepare_sections", "actor_session_id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10", "client_mutation_id": "c9f0f895-fb98-4b91-9e3f-1d2a6b7c8d20", "section_ids": [ "45c48cce-2e2d-4fbd-8a1c-5e6f7a8b9c30" ] }' ``` ### Get the latest project workflow `GET /studio/projects/{projectId}/workflows` Scope: `studio:read` · Operation ID: `getLatestStudioWorkflow` Returns the project's most recent workflow with its steps, or `null` if none has run. Pass `section_id` to get the most recent workflow that includes that section. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | #### Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `section_id` | string, uuid | No | Restrict to the workflow that most recently added a step for this section. Returns null when none exists or that workflow belongs to another project. | #### Response 200 Latest project workflow or null when none exists. Fields inside `data`: - `run` (object; required): Durable Studio project workflow run. - `id` (string, uuid; required): Workflow run ID. - `project_id` (string, uuid; required) - `requested_by_user_id` (string, uuid; required) - `payer_user_id` (string | null, uuid; required): User billed for Create Audio work; null for prepare_sections workflows. - `workflow_kind` (string, one of `prepare_sections`, `create_audio`; required) - `automation_profile` (string | null, one of `quick`, `balanced`, `directed`; required): Create Audio profile label from profile_snapshot.profile; null for prepare_sections. - `profile_snapshot` (object; required): Empty object for prepare_sections workflows; the frozen Create Audio options for create_audio workflows. - `status` (string, one of `queued`, `running`, `completed`, `partially_completed`, `failed`, `canceling`, `cancelled`; required): Workflow run status, recomputed from step statuses after every step transition. Non-terminal: queued (unfinished steps remain and none is active), running (at least one step is active), canceling (cancellation was requested and unfinished steps remain). Terminal (completed_at is set and the status no longer changes): completed (every step succeeded or was skipped), partially_completed (at least one failed or conflict step and at least one succeeded or skipped step), failed (failed or conflict steps and no succeeded or skipped step), cancelled (all steps finished after cancellation was requested). - `current_stage` (string | null, one of `preparing_sections`, `assigning_speakers`, `casting_voices`, `planning_emotion`, `directing_delivery`, `placing_project_sounds`, `directing_sfx`, `preparing_voices`, `generating_audio`; required): Stage of the next unfinished step while the run is queued, running, or canceling. Null once the run is terminal, or when the next unfinished step is render_section. - `step_count` (integer, 1–100; required) - `completed_step_count` (integer, 0–100; required): Steps in a terminal status (succeeded, skipped, failed, conflict, cancelled). - `progress` (object; required): Step counts by status. Currently contains integer counts pending, active, succeeded (succeeded or skipped), failed (failed or conflict), cancelled, completed (all terminal), and total; treat unknown keys as additive. - `pending` (integer, ≥ 0; optional) - `active` (integer, ≥ 0; optional) - `succeeded` (integer, ≥ 0; optional) - `failed` (integer, ≥ 0; optional) - `cancelled` (integer, ≥ 0; optional) - `completed` (integer, ≥ 0; optional) - `total` (integer, ≥ 0; optional) - `error_summary` (array of object, max 100 items; required): One entry per failed or conflict step in step order. Entries currently contain step_id, section_id, step_kind, code, and message; treat unknown keys as additive. - `step_id` (string, uuid; optional) - `section_id` (string, uuid; optional) - `step_kind` (string, one of `prepare_section`, `assign_speakers`, `cast_voices`, `direct_emotion`, `direct_section_delivery`, `place_project_sounds`, `direct_sfx`, `prepare_voices`, `generate_stale_audio`, `render_section`; optional): Kind of work a step performs. prepare_sections workflows contain only prepare_section steps. direct_section_delivery and render_section come from older Create Audio profiles (and successors resumed from them); new workflows do not plan them. - `code` (string | null; optional) - `message` (string | null; optional) - `cancel_requested_at` (string | null, date-time; required): When cancellation was first requested. - `cancel_acknowledged_at` (string | null, date-time; required): When the run reached cancelled. - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) - `started_at` (string | null, date-time; required): When execution first started; null before then. - `completed_at` (string | null, date-time; required): Set when the run reaches a terminal status (completed, partially_completed, failed, cancelled); null otherwise. - `resumes_workflow_run_id` (string | null, uuid; required): Predecessor run this run resumes; null for an original run. - `resumable` (boolean; required): True when status is failed or partially_completed and at least one step is failed or conflict. Only resumable runs can be resumed. - `steps` (array of object, 1–100 items; required): All steps in ascending step_index order. - `id` (string, uuid; required) - `workflow_run_id` (string, uuid; required) - `section_id` (string, uuid; required) - `step_key` (string, 1–200 chars; required): Stable key of the step within its workflow. - `step_index` (integer, 0–99; required): Zero-based position; steps are returned in ascending step_index order. - `step_kind` (string, one of `prepare_section`, `assign_speakers`, `cast_voices`, `direct_emotion`, `direct_section_delivery`, `place_project_sounds`, `direct_sfx`, `prepare_voices`, `generate_stale_audio`, `render_section`; required): Kind of work a step performs. prepare_sections workflows contain only prepare_section steps. direct_section_delivery and render_section come from older Create Audio profiles (and successors resumed from them); new workflows do not plan them. - `status` (string, one of `pending`, `active`, `succeeded`, `skipped`, `failed`, `conflict`, `cancelled`; required): Step status. pending and active are non-terminal; succeeded, skipped, failed, conflict, and cancelled are terminal. The run counts skipped as success and conflict as failure. conflict means the section document changed underneath the step. - `requested_document_version` (integer, ≥ 1; required): Section document version the step was planned against. - `execution_document_version` (integer | null, ≥ 1; required): Section document version the step executed against, when recorded. - `assistant_batch_id` (string | null, uuid; required): Child assistant batch started by an assistant-driven step. - `render_run_id` (string | null, uuid; required): Render run started by a render_section step. - `result_summary` (object; required): Step-specific result data, typed by the contract as an open JSON object. generate_stale_audio steps may carry the StudioWorkflowAudioResultSummary fields; steps cancelled before starting carry cancelled_before_start: true. - `issue_kind` (string | null, one of `document_changed`, `service_out_of_sync`, `assistant_failed`, `workflow_failed`; required): Set for failed and conflict steps; null for every other status. - `error_code` (string | null, 1–200 chars; required) - `error_message` (string | null, max 10000 chars; required) - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) - `started_at` (string | null, date-time; required) - `completed_at` (string | null, date-time; required) Errors: `400`, `401`, `403`, `500`, `503` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/workflows" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Get a workflow `GET /studio/workflows/{workflowRunId}` Scope: `studio:read` · Operation ID: `getStudioWorkflow` Returns `data: { run, steps }` with a workflow run and its ordered steps, the same payload shape as workflow creation and resume. Poll this endpoint until `data.run.status` is `completed`, `partially_completed`, `failed`, or `cancelled`. For each section's current per-block audio freshness, activity, errors, and playback URLs, read `GET /studio/sections/{sectionId}/media` and join by `block_id`; generation eligibility is a separate check. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `workflowRunId` | string, uuid | Yes | | #### Response 200 Current workflow and ordered section steps. Poll until run.status is terminal (completed, partially_completed, failed, cancelled). Fields inside `data`: - `run` (object; required): Durable Studio project workflow run. - `id` (string, uuid; required): Workflow run ID. - `project_id` (string, uuid; required) - `requested_by_user_id` (string, uuid; required) - `payer_user_id` (string | null, uuid; required): User billed for Create Audio work; null for prepare_sections workflows. - `workflow_kind` (string, one of `prepare_sections`, `create_audio`; required) - `automation_profile` (string | null, one of `quick`, `balanced`, `directed`; required): Create Audio profile label from profile_snapshot.profile; null for prepare_sections. - `profile_snapshot` (object; required): Empty object for prepare_sections workflows; the frozen Create Audio options for create_audio workflows. - `status` (string, one of `queued`, `running`, `completed`, `partially_completed`, `failed`, `canceling`, `cancelled`; required): Workflow run status, recomputed from step statuses after every step transition. Non-terminal: queued (unfinished steps remain and none is active), running (at least one step is active), canceling (cancellation was requested and unfinished steps remain). Terminal (completed_at is set and the status no longer changes): completed (every step succeeded or was skipped), partially_completed (at least one failed or conflict step and at least one succeeded or skipped step), failed (failed or conflict steps and no succeeded or skipped step), cancelled (all steps finished after cancellation was requested). - `current_stage` (string | null, one of `preparing_sections`, `assigning_speakers`, `casting_voices`, `planning_emotion`, `directing_delivery`, `placing_project_sounds`, `directing_sfx`, `preparing_voices`, `generating_audio`; required): Stage of the next unfinished step while the run is queued, running, or canceling. Null once the run is terminal, or when the next unfinished step is render_section. - `step_count` (integer, 1–100; required) - `completed_step_count` (integer, 0–100; required): Steps in a terminal status (succeeded, skipped, failed, conflict, cancelled). - `progress` (object; required): Step counts by status. Currently contains integer counts pending, active, succeeded (succeeded or skipped), failed (failed or conflict), cancelled, completed (all terminal), and total; treat unknown keys as additive. - `pending` (integer, ≥ 0; optional) - `active` (integer, ≥ 0; optional) - `succeeded` (integer, ≥ 0; optional) - `failed` (integer, ≥ 0; optional) - `cancelled` (integer, ≥ 0; optional) - `completed` (integer, ≥ 0; optional) - `total` (integer, ≥ 0; optional) - `error_summary` (array of object, max 100 items; required): One entry per failed or conflict step in step order. Entries currently contain step_id, section_id, step_kind, code, and message; treat unknown keys as additive. - `step_id` (string, uuid; optional) - `section_id` (string, uuid; optional) - `step_kind` (string, one of `prepare_section`, `assign_speakers`, `cast_voices`, `direct_emotion`, `direct_section_delivery`, `place_project_sounds`, `direct_sfx`, `prepare_voices`, `generate_stale_audio`, `render_section`; optional): Kind of work a step performs. prepare_sections workflows contain only prepare_section steps. direct_section_delivery and render_section come from older Create Audio profiles (and successors resumed from them); new workflows do not plan them. - `code` (string | null; optional) - `message` (string | null; optional) - `cancel_requested_at` (string | null, date-time; required): When cancellation was first requested. - `cancel_acknowledged_at` (string | null, date-time; required): When the run reached cancelled. - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) - `started_at` (string | null, date-time; required): When execution first started; null before then. - `completed_at` (string | null, date-time; required): Set when the run reaches a terminal status (completed, partially_completed, failed, cancelled); null otherwise. - `resumes_workflow_run_id` (string | null, uuid; required): Predecessor run this run resumes; null for an original run. - `resumable` (boolean; required): True when status is failed or partially_completed and at least one step is failed or conflict. Only resumable runs can be resumed. - `steps` (array of object, 1–100 items; required): All steps in ascending step_index order. - `id` (string, uuid; required) - `workflow_run_id` (string, uuid; required) - `section_id` (string, uuid; required) - `step_key` (string, 1–200 chars; required): Stable key of the step within its workflow. - `step_index` (integer, 0–99; required): Zero-based position; steps are returned in ascending step_index order. - `step_kind` (string, one of `prepare_section`, `assign_speakers`, `cast_voices`, `direct_emotion`, `direct_section_delivery`, `place_project_sounds`, `direct_sfx`, `prepare_voices`, `generate_stale_audio`, `render_section`; required): Kind of work a step performs. prepare_sections workflows contain only prepare_section steps. direct_section_delivery and render_section come from older Create Audio profiles (and successors resumed from them); new workflows do not plan them. - `status` (string, one of `pending`, `active`, `succeeded`, `skipped`, `failed`, `conflict`, `cancelled`; required): Step status. pending and active are non-terminal; succeeded, skipped, failed, conflict, and cancelled are terminal. The run counts skipped as success and conflict as failure. conflict means the section document changed underneath the step. - `requested_document_version` (integer, ≥ 1; required): Section document version the step was planned against. - `execution_document_version` (integer | null, ≥ 1; required): Section document version the step executed against, when recorded. - `assistant_batch_id` (string | null, uuid; required): Child assistant batch started by an assistant-driven step. - `render_run_id` (string | null, uuid; required): Render run started by a render_section step. - `result_summary` (object; required): Step-specific result data, typed by the contract as an open JSON object. generate_stale_audio steps may carry the StudioWorkflowAudioResultSummary fields; steps cancelled before starting carry cancelled_before_start: true. - `issue_kind` (string | null, one of `document_changed`, `service_out_of_sync`, `assistant_failed`, `workflow_failed`; required): Set for failed and conflict steps; null for every other status. - `error_code` (string | null, 1–200 chars; required) - `error_message` (string | null, max 10000 chars; required) - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) - `started_at` (string | null, date-time; required) - `completed_at` (string | null, date-time; required) Errors: `400`, `401`, `403`, `404`, `500`, `503` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/workflows/$WORKFLOW_RUN_ID" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Get workflow audio progress `GET /studio/workflows/{workflowRunId}/progress` Scope: `studio:read` · Operation ID: `getStudioWorkflowProgress` Returns ready/total audio clip counts. The default `schema_version=1` response is unchanged. Request `?schema_version=2` for failed-block issues with `section_id`, stable `block_id`, legacy one-based `block_number`, and zero-based `block_index` in the full document order captured when the issue was recorded. Use IDs to locate blocks after edits, not old indexes or text-chunk counters. Inspect each block's current freshness, activity, errors, and playback URL through `GET /studio/sections/{sectionId}/media`; `generation-availability` does not report current audio. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `workflowRunId` | string, uuid | Yes | | #### Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `schema_version` | integer, default 1, one of `1`, `2` | No | 1 (default) returns counts only. 2 adds failed-block issues using stable IDs and captured-document zero-based indexes. | #### Response 200 Current workflow audio counts. Fields inside `data`: - One of: - **schema_version: 1** - `schema_version` (1; required) - `workflow_run_id` (string, uuid; required) - `ready` (integer, ≥ 0; required) - `total` (integer, ≥ 0; required) - **schema_version: 2** - `schema_version` (2; required) - `workflow_run_id` (string, uuid; required) - `ready` (integer, ≥ 0; required) - `total` (integer, ≥ 0; required) - `block_issues` (array of object, max 2000 items; required) - `block_id` (string, uuid; required) - `block_number` (integer, 1–50000; required) - `block_kind` (string, one of `speech`, `narration`, `sfx`, `unparsed`, `excluded`; required) - `speaker_name` (string | null, 1–200 chars; required) - `reason` (string, 1–1000 chars; required) - `recovery` (string, 1–1000 chars; required) - `section_id` (string, uuid; required) - `block_index` (integer, 0–49999; required): Zero-based position in the full document order captured when the issue was recorded. Join current resources using block_id, not an index after edits. Errors: `400`, `401`, `403`, `404`, `500` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/workflows/$WORKFLOW_RUN_ID/progress" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Cancel a workflow `POST /studio/workflows/{workflowRunId}/cancel` Scope: `studio:write` · Operation ID: `cancelStudioWorkflow` Requests cancellation of a prepare or Create Audio workflow. Pending steps are cancelled immediately; running steps stop cooperatively and their late results are discarded. A finished workflow is returned unchanged. Send no request body. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `workflowRunId` | string, uuid | Yes | | #### Response 200 Current workflow state, normally canceling or cancelled. Fields inside `data`: - `run` (object; required): Durable Studio project workflow run. - `id` (string, uuid; required): Workflow run ID. - `project_id` (string, uuid; required) - `requested_by_user_id` (string, uuid; required) - `payer_user_id` (string | null, uuid; required): User billed for Create Audio work; null for prepare_sections workflows. - `workflow_kind` (string, one of `prepare_sections`, `create_audio`; required) - `automation_profile` (string | null, one of `quick`, `balanced`, `directed`; required): Create Audio profile label from profile_snapshot.profile; null for prepare_sections. - `profile_snapshot` (object; required): Empty object for prepare_sections workflows; the frozen Create Audio options for create_audio workflows. - `status` (string, one of `queued`, `running`, `completed`, `partially_completed`, `failed`, `canceling`, `cancelled`; required): Workflow run status, recomputed from step statuses after every step transition. Non-terminal: queued (unfinished steps remain and none is active), running (at least one step is active), canceling (cancellation was requested and unfinished steps remain). Terminal (completed_at is set and the status no longer changes): completed (every step succeeded or was skipped), partially_completed (at least one failed or conflict step and at least one succeeded or skipped step), failed (failed or conflict steps and no succeeded or skipped step), cancelled (all steps finished after cancellation was requested). - `current_stage` (string | null, one of `preparing_sections`, `assigning_speakers`, `casting_voices`, `planning_emotion`, `directing_delivery`, `placing_project_sounds`, `directing_sfx`, `preparing_voices`, `generating_audio`; required): Stage of the next unfinished step while the run is queued, running, or canceling. Null once the run is terminal, or when the next unfinished step is render_section. - `step_count` (integer, 1–100; required) - `completed_step_count` (integer, 0–100; required): Steps in a terminal status (succeeded, skipped, failed, conflict, cancelled). - `progress` (object; required): Step counts by status. Currently contains integer counts pending, active, succeeded (succeeded or skipped), failed (failed or conflict), cancelled, completed (all terminal), and total; treat unknown keys as additive. - `pending` (integer, ≥ 0; optional) - `active` (integer, ≥ 0; optional) - `succeeded` (integer, ≥ 0; optional) - `failed` (integer, ≥ 0; optional) - `cancelled` (integer, ≥ 0; optional) - `completed` (integer, ≥ 0; optional) - `total` (integer, ≥ 0; optional) - `error_summary` (array of object, max 100 items; required): One entry per failed or conflict step in step order. Entries currently contain step_id, section_id, step_kind, code, and message; treat unknown keys as additive. - `step_id` (string, uuid; optional) - `section_id` (string, uuid; optional) - `step_kind` (string, one of `prepare_section`, `assign_speakers`, `cast_voices`, `direct_emotion`, `direct_section_delivery`, `place_project_sounds`, `direct_sfx`, `prepare_voices`, `generate_stale_audio`, `render_section`; optional): Kind of work a step performs. prepare_sections workflows contain only prepare_section steps. direct_section_delivery and render_section come from older Create Audio profiles (and successors resumed from them); new workflows do not plan them. - `code` (string | null; optional) - `message` (string | null; optional) - `cancel_requested_at` (string | null, date-time; required): When cancellation was first requested. - `cancel_acknowledged_at` (string | null, date-time; required): When the run reached cancelled. - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) - `started_at` (string | null, date-time; required): When execution first started; null before then. - `completed_at` (string | null, date-time; required): Set when the run reaches a terminal status (completed, partially_completed, failed, cancelled); null otherwise. - `resumes_workflow_run_id` (string | null, uuid; required): Predecessor run this run resumes; null for an original run. - `resumable` (boolean; required): True when status is failed or partially_completed and at least one step is failed or conflict. Only resumable runs can be resumed. - `steps` (array of object, 1–100 items; required): All steps in ascending step_index order. - `id` (string, uuid; required) - `workflow_run_id` (string, uuid; required) - `section_id` (string, uuid; required) - `step_key` (string, 1–200 chars; required): Stable key of the step within its workflow. - `step_index` (integer, 0–99; required): Zero-based position; steps are returned in ascending step_index order. - `step_kind` (string, one of `prepare_section`, `assign_speakers`, `cast_voices`, `direct_emotion`, `direct_section_delivery`, `place_project_sounds`, `direct_sfx`, `prepare_voices`, `generate_stale_audio`, `render_section`; required): Kind of work a step performs. prepare_sections workflows contain only prepare_section steps. direct_section_delivery and render_section come from older Create Audio profiles (and successors resumed from them); new workflows do not plan them. - `status` (string, one of `pending`, `active`, `succeeded`, `skipped`, `failed`, `conflict`, `cancelled`; required): Step status. pending and active are non-terminal; succeeded, skipped, failed, conflict, and cancelled are terminal. The run counts skipped as success and conflict as failure. conflict means the section document changed underneath the step. - `requested_document_version` (integer, ≥ 1; required): Section document version the step was planned against. - `execution_document_version` (integer | null, ≥ 1; required): Section document version the step executed against, when recorded. - `assistant_batch_id` (string | null, uuid; required): Child assistant batch started by an assistant-driven step. - `render_run_id` (string | null, uuid; required): Render run started by a render_section step. - `result_summary` (object; required): Step-specific result data, typed by the contract as an open JSON object. generate_stale_audio steps may carry the StudioWorkflowAudioResultSummary fields; steps cancelled before starting carry cancelled_before_start: true. - `issue_kind` (string | null, one of `document_changed`, `service_out_of_sync`, `assistant_failed`, `workflow_failed`; required): Set for failed and conflict steps; null for every other status. - `error_code` (string | null, 1–200 chars; required) - `error_message` (string | null, max 10000 chars; required) - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) - `started_at` (string | null, date-time; required) - `completed_at` (string | null, date-time; required) Errors: `400`, `401`, `403`, `404`, `409`, `500`, `503` return the standard error envelope. #### Example request ```bash curl -X POST "https://lyricwinter.com/api/v1/studio/workflows/$WORKFLOW_RUN_ID/cancel" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Resume a failed workflow `POST /studio/workflows/{workflowRunId}/resume` Scope: `studio:write` · Retries: reuse the same `client_mutation_id` · Operation ID: `resumeStudioWorkflow` Starts a new workflow linked to a failed or partially completed run and returns `202` with `data: { run, steps }`, the same shape as creation and polling. It picks up from each section's first failed stage instead of redoing finished work. Only runs with `resumable: true` can be resumed. If recovery returns a retryable error, retry the same body and `client_mutation_id` to preserve the original recovery request. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `workflowRunId` | string, uuid | Yes | | #### Request body (application/json, required) - `schema_version` (1; required) - `actor_session_id` (string, uuid; required): Client session that issued the request. - `client_mutation_id` (string, uuid; required): Idempotency key; the successor workflow ID is derived from it. Reusing it for different content returns 409. #### Response 202 The linked successor workflow and its recovery steps. Fields inside `data`: - `run` (object; required): Durable Studio project workflow run. - `id` (string, uuid; required): Workflow run ID. - `project_id` (string, uuid; required) - `requested_by_user_id` (string, uuid; required) - `payer_user_id` (string | null, uuid; required): User billed for Create Audio work; null for prepare_sections workflows. - `workflow_kind` (string, one of `prepare_sections`, `create_audio`; required) - `automation_profile` (string | null, one of `quick`, `balanced`, `directed`; required): Create Audio profile label from profile_snapshot.profile; null for prepare_sections. - `profile_snapshot` (object; required): Empty object for prepare_sections workflows; the frozen Create Audio options for create_audio workflows. - `status` (string, one of `queued`, `running`, `completed`, `partially_completed`, `failed`, `canceling`, `cancelled`; required): Workflow run status, recomputed from step statuses after every step transition. Non-terminal: queued (unfinished steps remain and none is active), running (at least one step is active), canceling (cancellation was requested and unfinished steps remain). Terminal (completed_at is set and the status no longer changes): completed (every step succeeded or was skipped), partially_completed (at least one failed or conflict step and at least one succeeded or skipped step), failed (failed or conflict steps and no succeeded or skipped step), cancelled (all steps finished after cancellation was requested). - `current_stage` (string | null, one of `preparing_sections`, `assigning_speakers`, `casting_voices`, `planning_emotion`, `directing_delivery`, `placing_project_sounds`, `directing_sfx`, `preparing_voices`, `generating_audio`; required): Stage of the next unfinished step while the run is queued, running, or canceling. Null once the run is terminal, or when the next unfinished step is render_section. - `step_count` (integer, 1–100; required) - `completed_step_count` (integer, 0–100; required): Steps in a terminal status (succeeded, skipped, failed, conflict, cancelled). - `progress` (object; required): Step counts by status. Currently contains integer counts pending, active, succeeded (succeeded or skipped), failed (failed or conflict), cancelled, completed (all terminal), and total; treat unknown keys as additive. - `pending` (integer, ≥ 0; optional) - `active` (integer, ≥ 0; optional) - `succeeded` (integer, ≥ 0; optional) - `failed` (integer, ≥ 0; optional) - `cancelled` (integer, ≥ 0; optional) - `completed` (integer, ≥ 0; optional) - `total` (integer, ≥ 0; optional) - `error_summary` (array of object, max 100 items; required): One entry per failed or conflict step in step order. Entries currently contain step_id, section_id, step_kind, code, and message; treat unknown keys as additive. - `step_id` (string, uuid; optional) - `section_id` (string, uuid; optional) - `step_kind` (string, one of `prepare_section`, `assign_speakers`, `cast_voices`, `direct_emotion`, `direct_section_delivery`, `place_project_sounds`, `direct_sfx`, `prepare_voices`, `generate_stale_audio`, `render_section`; optional): Kind of work a step performs. prepare_sections workflows contain only prepare_section steps. direct_section_delivery and render_section come from older Create Audio profiles (and successors resumed from them); new workflows do not plan them. - `code` (string | null; optional) - `message` (string | null; optional) - `cancel_requested_at` (string | null, date-time; required): When cancellation was first requested. - `cancel_acknowledged_at` (string | null, date-time; required): When the run reached cancelled. - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) - `started_at` (string | null, date-time; required): When execution first started; null before then. - `completed_at` (string | null, date-time; required): Set when the run reaches a terminal status (completed, partially_completed, failed, cancelled); null otherwise. - `resumes_workflow_run_id` (string | null, uuid; required): Predecessor run this run resumes; null for an original run. - `resumable` (boolean; required): True when status is failed or partially_completed and at least one step is failed or conflict. Only resumable runs can be resumed. - `steps` (array of object, 1–100 items; required): All steps in ascending step_index order. - `id` (string, uuid; required) - `workflow_run_id` (string, uuid; required) - `section_id` (string, uuid; required) - `step_key` (string, 1–200 chars; required): Stable key of the step within its workflow. - `step_index` (integer, 0–99; required): Zero-based position; steps are returned in ascending step_index order. - `step_kind` (string, one of `prepare_section`, `assign_speakers`, `cast_voices`, `direct_emotion`, `direct_section_delivery`, `place_project_sounds`, `direct_sfx`, `prepare_voices`, `generate_stale_audio`, `render_section`; required): Kind of work a step performs. prepare_sections workflows contain only prepare_section steps. direct_section_delivery and render_section come from older Create Audio profiles (and successors resumed from them); new workflows do not plan them. - `status` (string, one of `pending`, `active`, `succeeded`, `skipped`, `failed`, `conflict`, `cancelled`; required): Step status. pending and active are non-terminal; succeeded, skipped, failed, conflict, and cancelled are terminal. The run counts skipped as success and conflict as failure. conflict means the section document changed underneath the step. - `requested_document_version` (integer, ≥ 1; required): Section document version the step was planned against. - `execution_document_version` (integer | null, ≥ 1; required): Section document version the step executed against, when recorded. - `assistant_batch_id` (string | null, uuid; required): Child assistant batch started by an assistant-driven step. - `render_run_id` (string | null, uuid; required): Render run started by a render_section step. - `result_summary` (object; required): Step-specific result data, typed by the contract as an open JSON object. generate_stale_audio steps may carry the StudioWorkflowAudioResultSummary fields; steps cancelled before starting carry cancelled_before_start: true. - `issue_kind` (string | null, one of `document_changed`, `service_out_of_sync`, `assistant_failed`, `workflow_failed`; required): Set for failed and conflict steps; null for every other status. - `error_code` (string | null, 1–200 chars; required) - `error_message` (string | null, max 10000 chars; required) - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) - `started_at` (string | null, date-time; required) - `completed_at` (string | null, date-time; required) Errors: `400`, `401`, `403`, `404`, `409`, `500`, `503` return the standard error envelope. #### Example request ```bash curl -X POST "https://lyricwinter.com/api/v1/studio/workflows/$WORKFLOW_RUN_ID/resume" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schema_version": 1, "actor_session_id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10", "client_mutation_id": "c9f0f895-fb98-4b91-9e3f-1d2a6b7c8d20" }' ``` --- Source: https://lyricwinter.com/docs/api/media.md --- title: "Media and playback API" description: "Check whether generation can start separately from whether audio already exists. Read per-block media freshness and generation activity, compile a playback manifest for current audio, and mint short-lived URLs. Generation availability checks voice/compiler eligibility and confirmed ElevenLabs preparation exhaustion; it is not a promise of audio, synthesis quota, or funding." canonical_url: https://lyricwinter.com/docs/api/media markdown_url: https://lyricwinter.com/docs/api/media.md last_updated: 2026-10-04 status: beta --- # LyricWinter API reference: Media and playback > Check whether generation can start separately from whether audio already exists. Read per-block media freshness and generation activity, compile a playback manifest for current audio, and mint short-lived URLs. Generation availability checks voice/compiler eligibility and confirmed ElevenLabs preparation exhaustion; it is not a promise of audio, synthesis quota, or funding. The LyricWinter API is in beta. Base URL: `https://lyricwinter.com/api/v1`. Authenticate every request with `Authorization: Bearer lw_...`. Successful responses are wrapped as `{ "data": ..., "request_id": "req_..." }`. Machine-readable contract: https://lyricwinter.com/api/v1/openapi.json. ## Endpoints - [Get section media](https://lyricwinter.com/docs/api/media#get-studio-section-media): `GET /studio/sections/{sectionId}/media` - [Check generation eligibility](https://lyricwinter.com/docs/api/media#get-studio-section-generation-availability): `GET /studio/sections/{sectionId}/generation-availability` - [Get a playback manifest](https://lyricwinter.com/docs/api/media#get-studio-playback-manifest): `GET /studio/sections/{sectionId}/playback-manifest` - [Create a media access URL](https://lyricwinter.com/docs/api/media#create-studio-media-access-url): `POST /studio/media-assets/{assetId}/access-url` ### Get section media `GET /studio/sections/{sectionId}/media` Scope: `studio:read` · Operation ID: `getStudioSectionMedia` Returns one entry per document block with audio freshness, independent generation activity, the latest generation/import error, and a short-lived signed playback_url for active audio. A failed retake can coexist with current playable audio. Available generation does not imply audio exists. Join by block_id. Request ?schema_version=2 for boundary_silence_handling from the active take's stored intent: timeline, fixed_in_asset, unsupported, or unavailable. The default response remains unchanged. Timeline handling applies only to boundary pauses; internal timed silences remain part of generated audio. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `sectionId` | string, uuid | Yes | | #### Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `schema_version` | integer, default 1, one of `1`, `2` | No | 1 or omitted preserves the existing unversioned payload. 2 returns active-take boundary_silence_handling. Other values return 400. | #### Response 200 Current block media read model; schema_version=2 additionally describes active-take boundary silence handling. Fields inside `data`: - One of: - **StudioSectionMediaPayload**: Current per-block media state for a Studio section. - `section_id` (string, uuid; required) - `playback_duration_ms` (integer | null, ≥ 0; required): Duration of the newest saved render manifest for the section's current document and speaker-registry versions; null when none exists. - `blocks` (array of object; required): One entry per document block, in document order. - `block_id` (string, uuid; required) - `freshness` (string, one of `absent`, `current`, `stale`, `unsupported`; required): absent: no active rendition. current: the active rendition matches the block's current generation intent. stale: it differs (see stale_reasons). unsupported: the block kind is not speech, narration, or sfx. - `activity` (string, one of `idle`, `generating`, `failed`; required): generating: a pending rendition is queued, running, or waiting to retry, or imported audio is being copied. failed: the latest request or import failed. idle otherwise. Independent of freshness. - `active_block_version` (integer | null, ≥ 1; required): Block version the active rendition was requested for. - `requested_block_version` (integer | null, ≥ 1; required): Block version of the pending rendition or import, if any. - `active_rendition_id` (string | null, uuid; required) - `audio_version_count` (integer, ≥ 0; required): Number of completed audio versions for the block. - `generation_batch_id` (string | null, uuid; required): Generation batch of the pending rendition. - `playback_url` (string | null, uri; required): Short-lived signed URL for the active rendition's audio; null without an active asset. - `playback_url_expires_at` (string | null, date-time; required) - `content_type` (string | null; required) - `size_bytes` (integer | null, ≥ 1; required) - `analysis_integrated_lufs` (number | null, -120–24; required) - `analysis_sample_peak_dbfs` (number | null, -200–24; required) - `provider` (string | null; required): Provider that actually produced the active rendition (stored metadata for imports). - `model` (string | null; required): Model that actually produced the active rendition (stored metadata for imports). - `error` (string | null; required): Latest generation or import error message. - `stale_reasons` (array of string, one of `block_kind`, `text`, `pronunciation`, `excluded_text`, `timing`, `voice`, `generation_profile`, `delivery_controls`, `sound_effect`; optional): Present for speech, narration, and sfx blocks; non-empty only when freshness is stale. - **schema_version: 2**: Opt-in media response with authoritative active-take boundary silence handling. - `schema_version` (2; required) - `section_id` (string, uuid; required) - `playback_duration_ms` (integer | null, ≥ 0; optional) - `blocks` (array of object; required) - `block_id` (string, uuid; required) - `freshness` (string, one of `absent`, `current`, `stale`, `unsupported`; required): absent: no active rendition. current: the active rendition matches the block's current generation intent. stale: it differs (see stale_reasons). unsupported: the block kind is not speech, narration, or sfx. - `activity` (string, one of `idle`, `generating`, `failed`; required): generating: a pending rendition is queued, running, or waiting to retry, or imported audio is being copied. failed: the latest request or import failed. idle otherwise. Independent of freshness. - `active_block_version` (integer | null, ≥ 1; required): Block version the active rendition was requested for. - `requested_block_version` (integer | null, ≥ 1; required): Block version of the pending rendition or import, if any. - `active_rendition_id` (string | null, uuid; required) - `audio_version_count` (integer, ≥ 0; required): Number of completed audio versions for the block. - `generation_batch_id` (string | null, uuid; required): Generation batch of the pending rendition. - `playback_url` (string | null, uri; required): Short-lived signed URL for the active rendition's audio; null without an active asset. - `playback_url_expires_at` (string | null, date-time; required) - `content_type` (string | null; required) - `size_bytes` (integer | null, ≥ 1; required) - `analysis_integrated_lufs` (number | null, -120–24; required) - `analysis_sample_peak_dbfs` (number | null, -200–24; required) - `provider` (string | null; required): Provider that actually produced the active rendition (stored metadata for imports). - `model` (string | null; required): Model that actually produced the active rendition (stored metadata for imports). - `error` (string | null; required): Latest generation or import error message. - `stale_reasons` (array of string, one of `block_kind`, `text`, `pronunciation`, `excluded_text`, `timing`, `voice`, `generation_profile`, `delivery_controls`, `sound_effect`; optional): Present for speech, narration, and sfx blocks; non-empty only when freshness is stale. - `boundary_silence_handling` (string, one of `timeline`, `fixed_in_asset`, `unsupported`, `unavailable`; required): Boundary silence handling of the active take, not a promise about internal pauses. timeline: stored intent uses timeline-v1, so boundary pauses are assembled outside synthesis. fixed_in_asset: generated speech uses legacy baked timing. unsupported: the active media does not expose supported boundary-pause semantics. unavailable: no active media. Internal timed silences remain fixed in the asset even with timeline handling. Errors: `400`, `401`, `403`, `404`, `500`, `503` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/sections/$SECTION_ID/media" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Check generation eligibility `GET /studio/sections/{sectionId}/generation-availability` Scope: `studio:read` · Operation ID: `getStudioSectionGenerationAvailability` Returns each block's effective requested profile, voice/provider materialization requirements, and compiler diagnostics. Confirmed ElevenLabs preparation exhaustion makes an unprepared ElevenLabs option unavailable; already prepared current-revision voices remain eligible. `generation.status: available` does not mean audio exists or guarantee live synthesis quota or funding. Use `/media` for freshness, activity, errors, and playable audio. Pinned project_sound blocks use library materialization rather than synthesis and have generation: null, as do blocks without provider generation. Null does not claim a sound is present or playable. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `sectionId` | string, uuid | Yes | | #### Response 200 Current block generation eligibility, not existing-audio readiness or live provider-account health. Fields inside `data`: - `section_id` (string, uuid; required) - `document_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `blocks` (array of object; required) - `block_id` (string, uuid; required) - `generation` (object | null; required): Whether generation can be requested under current voice/compiler authority, including confirmed ElevenLabs voice-preparation exhaustion for uncached voices. Available does not mean audio exists; check /media freshness, activity, and playback_url. This is not a general provider synthesis quota or billing check. - `requested_profile` (object; required): Saved profile read shape, including historical options. For new full-profile requests use StudioGenerationProfileRequest; its provider/model branches list accepted option keys. Generation eligibility and actual audio readiness are separate reads. - `provider` (string, pattern ^[a-z][a-z0-9_-]{0,63}$; required) - `model` (string, 1–200 chars; required) - `fallback` (string, one of `strict`, `best_effort`; required) - `controls` (string, one of `strict`, `best_effort`; required) - `options` (object; required) - `status` (string, one of `available`, `materialization_required`, `unavailable`; required) - `reason` (string | null; required) - `reason_code` (string, one of `provider_asset_missing`, `acoustic_revision_changed`; optional) - `diagnostics` (array of object; required) - `code` (string, min 1 chars; required) - `severity` (string, one of `warning`, `error`; required) - `message` (string, min 1 chars; required) - `options` (array of object; required) - `provider` (string, min 1 chars; required) - `model` (string, min 1 chars; required) - `status` (string, one of `available`, `materialization_required`, `unavailable`; required) - `reason` (string | null; required) - `reason_code` (string, one of `provider_asset_missing`, `acoustic_revision_changed`; optional) - `diagnostics` (array of object; required) - `code` (object; required) - `severity` (object; required) - `message` (object; required) Errors: `400`, `401`, `403`, `404`, `500` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/sections/$SECTION_ID/generation-availability" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Get a playback manifest `GET /studio/sections/{sectionId}/playback-manifest` Scope: `studio:read` · Operation ID: `getStudioPlaybackManifest` Compiles the section's current speech, pauses, and sound-effect overlays into one timeline with signed asset URLs. Pass document_version and speaker_registry_version from the document. In sequential_units, unit_type: asset has the audio asset's own duration, unit_type: silence is an authored pause assembled separately on the timeline, and unit_type: spacing is an inter-block gap; sound effects are overlay_units. A speech or narration export block sums its contiguous asset and silence unit durations, excluding spacing, so do not equate its duration_ms with one asset unit's duration. Join blocks by stable block_id and per-unit records by unit_id, not by start_ms. Optional schema_version=2 adds unit_word_timings sidecars keyed by unit_id, block_id, and asset_id with provider-aligned section-relative words or word_timing: unavailable. Alignment is bound to the actual asset content hash; old/unsupported/invalid alignment is not estimated or transcribed. The default response and hashed audio manifest remain unchanged. Unknown query keys return 400. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `sectionId` | string, uuid | Yes | | #### Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `document_version` | integer, ≥ 1 | Yes | Saved section document version to compile. If it no longer matches the saved section, the request fails with 409 STUDIO_PLAYBACK_STALE. | | `schema_version` | integer, default 1, one of `1`, `2` | No | Opt in to provider word timing sidecars. The default retains the V1 response. | | `speaker_registry_version` | integer, ≥ 1 | Yes | Saved speaker-registry (cast) version to compile. If it no longer matches, the request fails with 409 STUDIO_PLAYBACK_STALE. | #### Response 200 Current private playback manifest and signed asset URLs. Fields inside `data`: - One of: - **schema_version: 1**: Ephemeral current playback manifest with signed asset URLs; no render run is created. - `schema_version` (1; required) - `mastering_version` (integer, ≥ 1; required) - `manifest_hash` (string, pattern ^[a-f0-9]{64}$; required): SHA-256 of the canonical manifest. - `manifest` (object; required): Compiled playback timeline for one saved section document and speaker-registry version. - `schema_version` ("studio-render-manifest-2"; required) - `project_id` (string, uuid; required) - `section_id` (string, uuid; required) - `document_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `mastering_version` (integer, ≥ 1; required) - `mastering_profile_hash` (string, pattern ^[a-f0-9]{64}$; required) - `mastering_profile` (object; required) - `schema_version` (1; required) - `enabled` (boolean; required) - `target_lufs` (number, -30–-10; required) - `strength` (number, 0–1; required) - `max_boost_db` (number, 0–24; required) - `max_reduction_db` (number, 0–24; required) - `silence_threshold_lufs` (number, -120–-20; required) - `peak_ceiling_db` (number, -12–-0.1; required) - `speaker_adjustments_db` (map of number; required) - `sfx_bus_gain_db` (number, -24–24, default 0; required) - `completeness` (string, one of `partial`, `complete`; required): complete exactly when gaps is empty. - `duration_ms` (integer, ≥ 0; required): Latest end point of any sequential or overlay unit. - `sequential_units` (array of object, max 50000 items; required): Contiguous from 0 ms in canonical block order; unit IDs are unique across sequential and overlay units. - One of: - **unit_type: "asset"** - `unit_id` (string, uuid; required) - `block_id` (string, uuid; required) - `block_index` (integer, ≥ 0; required) - `block_version` (integer, ≥ 1; required) - `block_kind` (string, one of `speech`, `narration`; required) - `start_ms` (integer, ≥ 0; required) - `duration_ms` (integer, ≥ 1; required) - `rendition_id` (string, uuid; required) - `rendition_intent_hash` (string, pattern ^[a-f0-9]{64}$; required) - `unit_type` ("asset"; required) - `asset_id` (string, uuid; required): Key into the payload's assets array. - `asset_content_hash` (string, pattern ^[a-f0-9]{64}$; required) - `source_ranges` (array of object, 1–10000 items; required) - `start_utf16` (integer, ≥ 0; required) - `end_utf16` (integer, ≥ 0; required) - `speaker_id` (string, uuid; required) - `analysis_integrated_lufs` (number, -120–12; required) - `analysis_sample_peak_dbfs` (number, -200–12; required) - `effective_gain_db` (number, -24–24; required) - **unit_type: "silence"** - `unit_id` (string, uuid; required) - `block_id` (string, uuid; required) - `block_index` (integer, ≥ 0; required) - `block_version` (integer, ≥ 1; required) - `block_kind` (string, one of `speech`, `narration`; required) - `start_ms` (integer, ≥ 0; required) - `duration_ms` (integer, ≥ 1; required) - `rendition_id` (string, uuid; required) - `rendition_intent_hash` (string, pattern ^[a-f0-9]{64}$; required) - `unit_type` ("silence"; required) - **unit_type: "spacing", spacing_kind: "inter_passage"** - `unit_id` (string, uuid; required) - `block_id` (string, uuid; required) - `block_index` (integer, ≥ 0; required) - `block_version` (integer, ≥ 1; required) - `block_kind` (string, one of `speech`, `narration`; required) - `start_ms` (integer, ≥ 0; required) - `duration_ms` (integer, ≥ 1; required) - `unit_type` ("spacing"; required) - `spacing_kind` ("inter_passage"; required) - `overlay_units` (array of object, max 10000 items; required): Ordered by start_ms, block_index, block_id, unit_id. - `unit_type` ("sfx"; required) - `unit_id` (string, uuid; required) - `block_id` (string, uuid; required) - `block_index` (integer, ≥ 0; required) - `block_version` (integer, ≥ 1; required) - `intent_hash` (string, pattern ^[a-f0-9]{64}$; required) - `asset_id` (string, uuid; required): Key into the payload's assets array. - `asset_content_hash` (string, pattern ^[a-f0-9]{64}$; required) - `start_ms` (integer, ≥ 0; required) - `duration_ms` (integer, ≥ 1; required) - `gain_db` (number, -60–24; required) - `fade_in_ms` (integer, ≥ 0; required) - `fade_out_ms` (integer, ≥ 0; required) - `timed_text_units` (array of object, max 50000 items; required): Ordered by start_ms, block_index, block_id, unit_id. - `unit_id` (string, uuid; required) - `block_id` (string, uuid; required) - `block_index` (integer, ≥ 0; required) - `block_version` (integer, ≥ 1; required) - `speaker_id` (string, uuid; required) - `start_ms` (integer, ≥ 0; required) - `duration_ms` (integer, ≥ 1; required) - `text` (string, 1–100000 chars; required) - `text_hash` (string, pattern ^[a-f0-9]{64}$; required) - `source_ranges` (array of object, 1–10000 items; required) - `start_utf16` (integer, ≥ 0; required) - `end_utf16` (integer, ≥ 0; required) - `gaps` (array of object, max 50000 items; required): Ordered by block_index, media_kind, block_id. - One of: - **media_kind: "speech"** - `block_id` (string, uuid; required) - `block_index` (integer, ≥ 0; required) - `block_version` (integer, ≥ 1; required) - `media_kind` ("speech"; required) - `reason` (string, one of `missing`, `stale`, `failed`, `unsupported`, `unassigned`, `unresolved`; required) - `source_ranges` (array of object, 1–10000 items; required) - `start_utf16` (integer, ≥ 0; required) - `end_utf16` (integer, ≥ 0; required) - **media_kind: "sfx"** - `block_id` (string, uuid; required) - `block_index` (integer, ≥ 0; required) - `block_version` (integer, ≥ 1; required) - `media_kind` ("sfx"; required) - `reason` (string, one of `missing`, `stale`, `failed`, `unsupported`, `unplaced`; required) - `source_ranges` (array of any, max 0 items; required) - `assets` (array of object, max 60000 items; required): Exactly one entry per distinct asset_id referenced by asset sequential units and overlay units. - `asset_id` (string, uuid; required) - `url` (string, uri; required): Short-lived signed inline URL. - `expires_at` (string, date-time; required) - `content_type` (string, min 1 chars; required) - `size_bytes` (integer, ≥ 1; required) - **schema_version: 2**: Opt-in playback payload with take-bound word timing sidecars. manifest_hash still hashes the unchanged audio manifest, not these optional sidecars. - `schema_version` (2; required) - `mastering_version` (integer, ≥ 1; required) - `manifest_hash` (string, pattern ^[a-f0-9]{64}$; required): SHA-256 of the canonical manifest. - `manifest` (object; required): Compiled playback timeline for one saved section document and speaker-registry version. - `schema_version` ("studio-render-manifest-2"; required) - `project_id` (string, uuid; required) - `section_id` (string, uuid; required) - `document_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `mastering_version` (integer, ≥ 1; required) - `mastering_profile_hash` (string, pattern ^[a-f0-9]{64}$; required) - `mastering_profile` (object; required) - `schema_version` (1; required) - `enabled` (boolean; required) - `target_lufs` (number, -30–-10; required) - `strength` (number, 0–1; required) - `max_boost_db` (number, 0–24; required) - `max_reduction_db` (number, 0–24; required) - `silence_threshold_lufs` (number, -120–-20; required) - `peak_ceiling_db` (number, -12–-0.1; required) - `speaker_adjustments_db` (map of number; required) - `sfx_bus_gain_db` (number, -24–24, default 0; required) - `completeness` (string, one of `partial`, `complete`; required): complete exactly when gaps is empty. - `duration_ms` (integer, ≥ 0; required): Latest end point of any sequential or overlay unit. - `sequential_units` (array of object, max 50000 items; required): Contiguous from 0 ms in canonical block order; unit IDs are unique across sequential and overlay units. - One of: - **unit_type: "asset"** - `unit_id` (string, uuid; required) - `block_id` (string, uuid; required) - `block_index` (integer, ≥ 0; required) - `block_version` (integer, ≥ 1; required) - `block_kind` (string, one of `speech`, `narration`; required) - `start_ms` (integer, ≥ 0; required) - `duration_ms` (integer, ≥ 1; required) - `rendition_id` (string, uuid; required) - `rendition_intent_hash` (string, pattern ^[a-f0-9]{64}$; required) - `unit_type` ("asset"; required) - `asset_id` (string, uuid; required): Key into the payload's assets array. - `asset_content_hash` (string, pattern ^[a-f0-9]{64}$; required) - `source_ranges` (array of object, 1–10000 items; required) - `start_utf16` (integer, ≥ 0; required) - `end_utf16` (integer, ≥ 0; required) - `speaker_id` (string, uuid; required) - `analysis_integrated_lufs` (number, -120–12; required) - `analysis_sample_peak_dbfs` (number, -200–12; required) - `effective_gain_db` (number, -24–24; required) - **unit_type: "silence"** - `unit_id` (string, uuid; required) - `block_id` (string, uuid; required) - `block_index` (integer, ≥ 0; required) - `block_version` (integer, ≥ 1; required) - `block_kind` (string, one of `speech`, `narration`; required) - `start_ms` (integer, ≥ 0; required) - `duration_ms` (integer, ≥ 1; required) - `rendition_id` (string, uuid; required) - `rendition_intent_hash` (string, pattern ^[a-f0-9]{64}$; required) - `unit_type` ("silence"; required) - **unit_type: "spacing", spacing_kind: "inter_passage"** - `unit_id` (string, uuid; required) - `block_id` (string, uuid; required) - `block_index` (integer, ≥ 0; required) - `block_version` (integer, ≥ 1; required) - `block_kind` (string, one of `speech`, `narration`; required) - `start_ms` (integer, ≥ 0; required) - `duration_ms` (integer, ≥ 1; required) - `unit_type` ("spacing"; required) - `spacing_kind` ("inter_passage"; required) - `overlay_units` (array of object, max 10000 items; required): Ordered by start_ms, block_index, block_id, unit_id. - `unit_type` ("sfx"; required) - `unit_id` (string, uuid; required) - `block_id` (string, uuid; required) - `block_index` (integer, ≥ 0; required) - `block_version` (integer, ≥ 1; required) - `intent_hash` (string, pattern ^[a-f0-9]{64}$; required) - `asset_id` (string, uuid; required): Key into the payload's assets array. - `asset_content_hash` (string, pattern ^[a-f0-9]{64}$; required) - `start_ms` (integer, ≥ 0; required) - `duration_ms` (integer, ≥ 1; required) - `gain_db` (number, -60–24; required) - `fade_in_ms` (integer, ≥ 0; required) - `fade_out_ms` (integer, ≥ 0; required) - `timed_text_units` (array of object, max 50000 items; required): Ordered by start_ms, block_index, block_id, unit_id. - `unit_id` (string, uuid; required) - `block_id` (string, uuid; required) - `block_index` (integer, ≥ 0; required) - `block_version` (integer, ≥ 1; required) - `speaker_id` (string, uuid; required) - `start_ms` (integer, ≥ 0; required) - `duration_ms` (integer, ≥ 1; required) - `text` (string, 1–100000 chars; required) - `text_hash` (string, pattern ^[a-f0-9]{64}$; required) - `source_ranges` (array of object, 1–10000 items; required) - `start_utf16` (integer, ≥ 0; required) - `end_utf16` (integer, ≥ 0; required) - `gaps` (array of object, max 50000 items; required): Ordered by block_index, media_kind, block_id. - One of: - **media_kind: "speech"** - `block_id` (string, uuid; required) - `block_index` (integer, ≥ 0; required) - `block_version` (integer, ≥ 1; required) - `media_kind` ("speech"; required) - `reason` (string, one of `missing`, `stale`, `failed`, `unsupported`, `unassigned`, `unresolved`; required) - `source_ranges` (array of object, 1–10000 items; required) - `start_utf16` (integer, ≥ 0; required) - `end_utf16` (integer, ≥ 0; required) - **media_kind: "sfx"** - `block_id` (string, uuid; required) - `block_index` (integer, ≥ 0; required) - `block_version` (integer, ≥ 1; required) - `media_kind` ("sfx"; required) - `reason` (string, one of `missing`, `stale`, `failed`, `unsupported`, `unplaced`; required) - `source_ranges` (array of any, max 0 items; required) - `assets` (array of object, max 60000 items; required): Exactly one entry per distinct asset_id referenced by asset sequential units and overlay units. - `asset_id` (string, uuid; required) - `url` (string, uri; required): Short-lived signed inline URL. - `expires_at` (string, date-time; required) - `content_type` (string, min 1 chars; required) - `size_bytes` (integer, ≥ 1; required) - `unit_word_timings` (array of object, max 60000 items; required) - One of: - **word_timing: "provider"** - `unit_id` (string, uuid; required) - `block_id` (string, uuid; required) - `asset_id` (string, uuid; required) - `word_timing` ("provider"; required) - `words` (array of object, 1–10000 items; required) - `text` (string, 1–4096 chars; required) - `start_ms` (integer, ≥ 0; required) - `end_ms` (integer, ≥ 0; required) - **word_timing: "unavailable"** - `unit_id` (string, uuid; required) - `block_id` (string, uuid; required) - `asset_id` (string, uuid; required) - `word_timing` ("unavailable"; required) Errors: `400`, `401`, `403`, `409`, `500`, `502`, `503` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/sections/$SECTION_ID/playback-manifest?document_version=1&speaker_registry_version=1" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Create a media access URL `POST /studio/media-assets/{assetId}/access-url` Scope: `studio:read` · Operation ID: `createStudioMediaAccessUrl` Returns a signed URL for one audio asset: six hours for inline playback or twelve hours for download. The URL supports HTTP range requests, so audio players can seek. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `assetId` | string, uuid | Yes | | #### Request body (application/json, required) - `schema_version` (1; required) - `disposition` (string, one of `inline`, `attachment`; required) - `filename` (string, 1–255 chars; optional) #### Response 200 Short-lived signed media URL and asset metadata. Fields inside `data`: - `schema_version` (1; required) - `asset_id` (string, uuid; required) - `url` (string, uri; required) - `expires_at` (string, date-time; required) - `content_type` (string, min 1 chars; required) - `size_bytes` (integer, ≥ 1; required) - `disposition` (string, one of `inline`, `attachment`; required) Errors: `400`, `401`, `403`, `404`, `409`, `500`, `503` return the standard error envelope. #### Example request ```bash curl -X POST "https://lyricwinter.com/api/v1/studio/media-assets/$ASSET_ID/access-url" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schema_version": 1, "disposition": "attachment", "filename": "Chapter-1.mp3" }' ``` --- Source: https://lyricwinter.com/docs/api/exports.md --- title: "Exports API" description: "Render one section or a project's selected sections into downloadable MP3, WAV, M4B, synchronized EPUB, or SRT files. Poll a project job for every child export, then download files individually and fetch each child's immutable manifest for matching timings. No ZIP is produced. Freshness compares document, cast, and mastering versions only, not newer audio takes or artifact readiness." canonical_url: https://lyricwinter.com/docs/api/exports markdown_url: https://lyricwinter.com/docs/api/exports.md last_updated: 2026-10-01 status: beta --- # LyricWinter API reference: Exports > Render one section or a project's selected sections into downloadable MP3, WAV, M4B, synchronized EPUB, or SRT files. Poll a project job for every child export, then download files individually and fetch each child's immutable manifest for matching timings. No ZIP is produced. Freshness compares document, cast, and mastering versions only, not newer audio takes or artifact readiness. The LyricWinter API is in beta. Base URL: `https://lyricwinter.com/api/v1`. Authenticate every request with `Authorization: Bearer lw_...`. Successful responses are wrapped as `{ "data": ..., "request_id": "req_..." }`. Machine-readable contract: https://lyricwinter.com/api/v1/openapi.json. ## Endpoints - [Get the mastering profile](https://lyricwinter.com/docs/api/exports#get-studio-mastering-profile): `GET /studio/projects/{projectId}/mastering-profile` - [Export a project's sections](https://lyricwinter.com/docs/api/exports#create-studio-project-export): `POST /studio/projects/{projectId}/exports` - [List project export jobs](https://lyricwinter.com/docs/api/exports#list-studio-project-exports): `GET /studio/projects/{projectId}/exports` - [Poll a project export job](https://lyricwinter.com/docs/api/exports#get-studio-project-export): `GET /studio/projects/{projectId}/exports/{exportId}` - [Compare section export versions](https://lyricwinter.com/docs/api/exports#get-studio-project-export-freshness): `GET /studio/projects/{projectId}/export-freshness` - [List exports](https://lyricwinter.com/docs/api/exports#list-studio-exports): `GET /studio/exports` - [Create an export](https://lyricwinter.com/docs/api/exports#create-studio-export): `POST /studio/exports` - [Get an export](https://lyricwinter.com/docs/api/exports#get-studio-export): `GET /studio/exports/{exportId}` - [Get an export's immutable timeline](https://lyricwinter.com/docs/api/exports#get-studio-export-manifest): `GET /studio/exports/{exportId}/manifest` - [Cancel an export](https://lyricwinter.com/docs/api/exports#cancel-studio-export): `POST /studio/exports/{exportId}/cancel` ### Get the mastering profile `GET /studio/projects/{projectId}/mastering-profile` Scope: `studio:read` · Operation ID: `getStudioMasteringProfile` Returns the project's loudness and mastering settings and its `mastering_version`, which exports require. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | #### Response 200 Current mastering authority and profile. Fields inside `data`: - `schema_version` (1; required) - `project_id` (string, uuid; required) - `project_kind` (string, one of `standalone`, `named`; required) - `mastering_version` (integer, ≥ 1; required) - `scope` (string, one of `document`, `project`; required) - `profile` (object; required) - `schema_version` (1; required) - `enabled` (boolean; required) - `target_lufs` (number, -30–-10; required) - `strength` (number, 0–1; required) - `max_boost_db` (number, 0–24; required) - `max_reduction_db` (number, 0–24; required) - `silence_threshold_lufs` (number, -120–-20; required) - `peak_ceiling_db` (number, -12–-0.1; required) - `speaker_adjustments_db` (map of number; required) - `sfx_bus_gain_db` (number, -24–24, default 0; required) - `sfx_adjustments` (object; optional) - `nonzero_count` (integer, ≥ 0; required) - `nonzero_section_count` (integer, ≥ 0; required) - `fingerprint` (string, pattern ^[a-f0-9]{64}$; required) Errors: `400`, `401`, `403`, `404`, `409`, `500` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/mastering-profile" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Export a project's sections `POST /studio/projects/{projectId}/exports` Scope: `studio:write` · Retries: send an `Idempotency-Key` header · Operation ID: `createStudioProjectExport` Creates a parent job with one section export per selected active section, in project order. Send `schema_version: 1`, `format`, and `pin: "current"` with an `Idempotency-Key` header. Optional `sections` selects 1-100 unique section UUIDs; omitting it selects all active sections, up to 100. The selection and saved document, cast, and mastering version plan are captured together and remain fixed. Audio takes are selected when each child compiles, not atomically at enqueue. Returns `202` for a new job or `200` for a database replay; an HTTP replay may retain the original `202`. Dispatch is asynchronous, without a fixed completion deadline. This does not generate speech or automatically retry failed paid generation. Poll the parent and download child files separately; `archive` is always null. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | #### Headers | Name | Type | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | string, 8–128 chars, pattern ^[\x21-\x7E]+$ | Yes | Key for one intended project export. Retry the same request with the same key; reuse for a different request returns 409. | #### Request body (application/json, required) - `schema_version` (1; required) - `format` (string, one of `mp3`, `wav`, `m4b`, `epub`, `srt`; required) - `pin` ("current"; required): Capture saved document, speaker registry, and mastering versions at enqueue. This does not capture an atomic queue-time media revision. - `sections` (array of string, 1–100 items, unique; optional): Active section IDs belonging to this project. Returned children follow project order, not request order. Omit to select all active sections; the resulting selection must contain one to 100 sections. #### Response 200 The existing parent was returned by database idempotency replay; this does not imply completion. Fields inside `data`: - `schema_version` (1; required) - `id` (string, uuid; required): Parent project export job ID. - `project_id` (string, uuid; required) - `format` (string, one of `mp3`, `wav`, `m4b`, `epub`, `srt`; required) - `status` (string, one of `queued`, `running`, `completed`, `partial_failed`, `failed`; required): queued when all children are queued; running while any child is active otherwise. Once all children are terminal or unavailable: completed if all succeeded, partial_failed if only some succeeded, or failed if none succeeded. Child cancellation, supersession, and unavailability count as failure, not successful completion. - `percent` (integer, 0–100; required): Floor of the mean child progress. Each terminal or unavailable child counts as 100, including failed, cancelled, or superseded children. Measures finished work, not success rate. - `created_at` (string, date-time; required) - `sections` (array of object, 1–100 items; required): Immutable selection and authority version plan in captured project order, with available child states or per-section unavailability errors. - `section_id` (string, uuid; required) - `title` (string, max 500 chars; required): Section title captured for this parent. - `export_id` (string, uuid; required): Captured child ID. Child status, manifest, and cancellation endpoints are usable only while the child remains accessible. - `authority` (object; required): Immutable saved versions captured at enqueue, retained even if the child is moved or deleted. - `document_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `mastering_version` (integer, ≥ 1; required) - `export` (object | null; required): Current ordinary child export payload, or null when the child is unavailable after transfer or deletion. - `schema_version` (1; required) - `id` (string, uuid; required) - `project_id` (string, uuid; required) - `section_id` (string, uuid; required) - `format` (string, one of `mp3`, `wav`, `m4b`, `epub`, `srt`; required) - `status` (string, one of `queued`, `running`, `retry_wait`, `canceling`, `completed`, `failed`, `cancelled`, `superseded`; required) - `phase` (string, one of `queued`, `compiling`, `claiming_artifact`, `downloading`, `mixing`, `encoding`, `packaging`, `uploading`, `complete`, `failed`, `cancelled`; required) - `percent` (integer, 0–100; required) - `cache_result` (string, one of `miss`, `reused`, `coalesced`; required) - `authority` (object; required) - `document_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `mastering_version` (integer, ≥ 1; required) - `is_current` (boolean; required) - `created_at` (string, date-time; required) - `started_at` (string | null, date-time; required) - `completed_at` (string | null, date-time; required) - `error` (object | null; required) - `code` (string; required) - `message` (string; required) - `artifact` (object | null; required) - `asset_id` (string, uuid; required) - `filename` (string, 1–255 chars; required) - `content_type` (string, min 1 chars; required) - `size_bytes` (integer, ≥ 1; required) - `download_url` (string, uri; required) - `download_url_expires_at` (string, date-time; required) - `artifact_expires_at` (string, date-time; required) - `error` (object | null; required): Per-section availability error, or null when the child is available. Unavailable children report STUDIO_PROJECT_EXPORT_SECTION_UNAVAILABLE and count as aggregate failure. - `code` (string, min 1 chars; required) - `message` (string; required) - `archive` (any | null; required): No combined archive or ZIP is produced. Download child artifacts individually. #### Response 202 New durable project export work accepted, or its original accepted HTTP response replayed. Poll for current state. Fields inside `data`: - `schema_version` (1; required) - `id` (string, uuid; required): Parent project export job ID. - `project_id` (string, uuid; required) - `format` (string, one of `mp3`, `wav`, `m4b`, `epub`, `srt`; required) - `status` (string, one of `queued`, `running`, `completed`, `partial_failed`, `failed`; required): queued when all children are queued; running while any child is active otherwise. Once all children are terminal or unavailable: completed if all succeeded, partial_failed if only some succeeded, or failed if none succeeded. Child cancellation, supersession, and unavailability count as failure, not successful completion. - `percent` (integer, 0–100; required): Floor of the mean child progress. Each terminal or unavailable child counts as 100, including failed, cancelled, or superseded children. Measures finished work, not success rate. - `created_at` (string, date-time; required) - `sections` (array of object, 1–100 items; required): Immutable selection and authority version plan in captured project order, with available child states or per-section unavailability errors. - `section_id` (string, uuid; required) - `title` (string, max 500 chars; required): Section title captured for this parent. - `export_id` (string, uuid; required): Captured child ID. Child status, manifest, and cancellation endpoints are usable only while the child remains accessible. - `authority` (object; required): Immutable saved versions captured at enqueue, retained even if the child is moved or deleted. - `document_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `mastering_version` (integer, ≥ 1; required) - `export` (object | null; required): Current ordinary child export payload, or null when the child is unavailable after transfer or deletion. - `schema_version` (1; required) - `id` (string, uuid; required) - `project_id` (string, uuid; required) - `section_id` (string, uuid; required) - `format` (string, one of `mp3`, `wav`, `m4b`, `epub`, `srt`; required) - `status` (string, one of `queued`, `running`, `retry_wait`, `canceling`, `completed`, `failed`, `cancelled`, `superseded`; required) - `phase` (string, one of `queued`, `compiling`, `claiming_artifact`, `downloading`, `mixing`, `encoding`, `packaging`, `uploading`, `complete`, `failed`, `cancelled`; required) - `percent` (integer, 0–100; required) - `cache_result` (string, one of `miss`, `reused`, `coalesced`; required) - `authority` (object; required) - `document_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `mastering_version` (integer, ≥ 1; required) - `is_current` (boolean; required) - `created_at` (string, date-time; required) - `started_at` (string | null, date-time; required) - `completed_at` (string | null, date-time; required) - `error` (object | null; required) - `code` (string; required) - `message` (string; required) - `artifact` (object | null; required) - `asset_id` (string, uuid; required) - `filename` (string, 1–255 chars; required) - `content_type` (string, min 1 chars; required) - `size_bytes` (integer, ≥ 1; required) - `download_url` (string, uri; required) - `download_url_expires_at` (string, date-time; required) - `artifact_expires_at` (string, date-time; required) - `error` (object | null; required): Per-section availability error, or null when the child is available. Unavailable children report STUDIO_PROJECT_EXPORT_SECTION_UNAVAILABLE and count as aggregate failure. - `code` (string, min 1 chars; required) - `message` (string; required) - `archive` (any | null; required): No combined archive or ZIP is produced. Download child artifacts individually. Errors: `400`, `401`, `403`, `404`, `409`, `500`, `503` return the standard error envelope. #### Example request ```bash curl -X POST "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/exports" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "schema_version": 1, "format": "mp3", "pin": "current" }' ``` ### List project export jobs `GET /studio/projects/{projectId}/exports` Scope: `studio:read` · Operation ID: `listStudioProjectExports` Lists parent jobs newest first, including their child exports. Optional `format` filters the list. `cursor` is an integer offset starting at 0; `limit` is 1-10 and defaults to 1. Pass the returned string `next_cursor` as the next offset; null means the end. Listing does not start exports or regenerate audio. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | #### Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `format` | string, one of `mp3`, `wav`, `m4b`, `epub`, `srt` | No | | | `cursor` | integer, ≥ 0, default 0 | No | | | `limit` | integer, 1–10, default 1 | No | | #### Response 200 A page of project export jobs and child statuses. Fields inside `data`: - `schema_version` (1; required) - `exports` (array of object, max 10 items; required) - `schema_version` (1; required) - `id` (string, uuid; required): Parent project export job ID. - `project_id` (string, uuid; required) - `format` (string, one of `mp3`, `wav`, `m4b`, `epub`, `srt`; required) - `status` (string, one of `queued`, `running`, `completed`, `partial_failed`, `failed`; required): queued when all children are queued; running while any child is active otherwise. Once all children are terminal or unavailable: completed if all succeeded, partial_failed if only some succeeded, or failed if none succeeded. Child cancellation, supersession, and unavailability count as failure, not successful completion. - `percent` (integer, 0–100; required): Floor of the mean child progress. Each terminal or unavailable child counts as 100, including failed, cancelled, or superseded children. Measures finished work, not success rate. - `created_at` (string, date-time; required) - `sections` (array of object, 1–100 items; required): Immutable selection and authority version plan in captured project order, with available child states or per-section unavailability errors. - `section_id` (string, uuid; required) - `title` (string, max 500 chars; required): Section title captured for this parent. - `export_id` (string, uuid; required): Captured child ID. Child status, manifest, and cancellation endpoints are usable only while the child remains accessible. - `authority` (object; required): Immutable saved versions captured at enqueue, retained even if the child is moved or deleted. - `document_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `mastering_version` (integer, ≥ 1; required) - `export` (object | null; required): Current ordinary child export payload, or null when the child is unavailable after transfer or deletion. - `schema_version` (1; required) - `id` (string, uuid; required) - `project_id` (string, uuid; required) - `section_id` (string, uuid; required) - `format` (string, one of `mp3`, `wav`, `m4b`, `epub`, `srt`; required) - `status` (string, one of `queued`, `running`, `retry_wait`, `canceling`, `completed`, `failed`, `cancelled`, `superseded`; required) - `phase` (string, one of `queued`, `compiling`, `claiming_artifact`, `downloading`, `mixing`, `encoding`, `packaging`, `uploading`, `complete`, `failed`, `cancelled`; required) - `percent` (integer, 0–100; required) - `cache_result` (string, one of `miss`, `reused`, `coalesced`; required) - `authority` (object; required) - `document_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `mastering_version` (integer, ≥ 1; required) - `is_current` (boolean; required) - `created_at` (string, date-time; required) - `started_at` (string | null, date-time; required) - `completed_at` (string | null, date-time; required) - `error` (object | null; required) - `code` (string; required) - `message` (string; required) - `artifact` (object | null; required) - `asset_id` (string, uuid; required) - `filename` (string, 1–255 chars; required) - `content_type` (string, min 1 chars; required) - `size_bytes` (integer, ≥ 1; required) - `download_url` (string, uri; required) - `download_url_expires_at` (string, date-time; required) - `artifact_expires_at` (string, date-time; required) - `error` (object | null; required): Per-section availability error, or null when the child is available. Unavailable children report STUDIO_PROJECT_EXPORT_SECTION_UNAVAILABLE and count as aggregate failure. - `code` (string, min 1 chars; required) - `message` (string; required) - `archive` (any | null; required): No combined archive or ZIP is produced. Download child artifacts individually. - `next_cursor` (string | null, min 1 chars; required): Next integer offset encoded as a string, or null at the end. Pass it as cursor with the same format filter. Errors: `400`, `401`, `403`, `404`, `500` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/exports" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Poll a project export job `GET /studio/projects/{projectId}/exports/{exportId}` Scope: `studio:read` · Operation ID: `getStudioProjectExport` Returns the parent job and ordered `sections`, each with `section_id`, captured `title`, child `export_id`, captured `authority` versions, nullable `export`, and nullable `error`. If a child is moved to another project or deleted, `export` is null and `error.code` is `STUDIO_PROJECT_EXPORT_SECTION_UNAVAILABLE`; the captured plan remains. Poll while `queued` or `running`. `completed`, `partial_failed`, and `failed` are terminal. Unavailable children count as failures; `percent` counts them and terminal failures as finished work, so 100 does not mean success. Download each available completed child's `export.artifact.download_url`; use its child ID for `/studio/exports/{exportId}/manifest` (including optional `schema_version=2` word timings) or `/cancel`. Transfers and cleanup are not blocked, and original child endpoints are not guaranteed to remain accessible. The parent ID is not a child ID. There is no combined archive or automatic re-export after edits. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | | `exportId` | string, uuid | Yes | The parent project export ID, not a child section export ID. | #### Response 200 Current project export parent and child states. Fields inside `data`: - `schema_version` (1; required) - `id` (string, uuid; required): Parent project export job ID. - `project_id` (string, uuid; required) - `format` (string, one of `mp3`, `wav`, `m4b`, `epub`, `srt`; required) - `status` (string, one of `queued`, `running`, `completed`, `partial_failed`, `failed`; required): queued when all children are queued; running while any child is active otherwise. Once all children are terminal or unavailable: completed if all succeeded, partial_failed if only some succeeded, or failed if none succeeded. Child cancellation, supersession, and unavailability count as failure, not successful completion. - `percent` (integer, 0–100; required): Floor of the mean child progress. Each terminal or unavailable child counts as 100, including failed, cancelled, or superseded children. Measures finished work, not success rate. - `created_at` (string, date-time; required) - `sections` (array of object, 1–100 items; required): Immutable selection and authority version plan in captured project order, with available child states or per-section unavailability errors. - `section_id` (string, uuid; required) - `title` (string, max 500 chars; required): Section title captured for this parent. - `export_id` (string, uuid; required): Captured child ID. Child status, manifest, and cancellation endpoints are usable only while the child remains accessible. - `authority` (object; required): Immutable saved versions captured at enqueue, retained even if the child is moved or deleted. - `document_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `mastering_version` (integer, ≥ 1; required) - `export` (object | null; required): Current ordinary child export payload, or null when the child is unavailable after transfer or deletion. - `schema_version` (1; required) - `id` (string, uuid; required) - `project_id` (string, uuid; required) - `section_id` (string, uuid; required) - `format` (string, one of `mp3`, `wav`, `m4b`, `epub`, `srt`; required) - `status` (string, one of `queued`, `running`, `retry_wait`, `canceling`, `completed`, `failed`, `cancelled`, `superseded`; required) - `phase` (string, one of `queued`, `compiling`, `claiming_artifact`, `downloading`, `mixing`, `encoding`, `packaging`, `uploading`, `complete`, `failed`, `cancelled`; required) - `percent` (integer, 0–100; required) - `cache_result` (string, one of `miss`, `reused`, `coalesced`; required) - `authority` (object; required) - `document_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `mastering_version` (integer, ≥ 1; required) - `is_current` (boolean; required) - `created_at` (string, date-time; required) - `started_at` (string | null, date-time; required) - `completed_at` (string | null, date-time; required) - `error` (object | null; required) - `code` (string; required) - `message` (string; required) - `artifact` (object | null; required) - `asset_id` (string, uuid; required) - `filename` (string, 1–255 chars; required) - `content_type` (string, min 1 chars; required) - `size_bytes` (integer, ≥ 1; required) - `download_url` (string, uri; required) - `download_url_expires_at` (string, date-time; required) - `artifact_expires_at` (string, date-time; required) - `error` (object | null; required): Per-section availability error, or null when the child is available. Unavailable children report STUDIO_PROJECT_EXPORT_SECTION_UNAVAILABLE and count as aggregate failure. - `code` (string, min 1 chars; required) - `message` (string; required) - `archive` (any | null; required): No combined archive or ZIP is produced. Download child artifacts individually. Errors: `400`, `401`, `403`, `404`, `500` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/exports/$EXPORT_ID" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Compare section export versions `GET /studio/projects/{projectId}/export-freshness` Scope: `studio:read` · Operation ID: `getStudioProjectExportFreshness` Returns active sections in project order, their saved document, speaker registry, and mastering versions, and their latest export for `format` (default `mp3`). Deleted sections are omitted; at most 100 active sections are supported. `basis` is `document_cast_mastering_versions`. Without an export, `latest_export` and `matches_current_authority` are null. A true match is only a version comparison: it does not guarantee latest audio takes, successful completion, complete media, or a valid downloadable artifact. Inspect status and artifact separately. This read does not export or generate anything. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | #### Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `format` | string, default "mp3", one of `mp3`, `wav`, `m4b`, `epub`, `srt` | No | | #### Response 200 Version comparisons for the project's active sections. Fields inside `data`: - `schema_version` (1; required) - `project_id` (string, uuid; required) - `format` (string, one of `mp3`, `wav`, `m4b`, `epub`, `srt`; required) - `basis` ("document_cast_mastering_versions"; required): Saved version comparison only; not take freshness, artifact validity, or successful completion. - `sections` (array of object, max 100 items; required): Active sections in project order; deleted sections are omitted. Projects exceeding 100 active sections return 400. - `section_id` (string, uuid; required) - `title` (string, max 500 chars; required) - `document_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `mastering_version` (integer, ≥ 1; required) - `latest_export` (object | null; required): Latest section export for the requested format, regardless of status; null if none exists. - `schema_version` (1; required) - `id` (string, uuid; required) - `project_id` (string, uuid; required) - `section_id` (string, uuid; required) - `format` (string, one of `mp3`, `wav`, `m4b`, `epub`, `srt`; required) - `status` (string, one of `queued`, `running`, `retry_wait`, `canceling`, `completed`, `failed`, `cancelled`, `superseded`; required) - `phase` (string, one of `queued`, `compiling`, `claiming_artifact`, `downloading`, `mixing`, `encoding`, `packaging`, `uploading`, `complete`, `failed`, `cancelled`; required) - `percent` (integer, 0–100; required) - `cache_result` (string, one of `miss`, `reused`, `coalesced`; required) - `authority` (object; required) - `document_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `mastering_version` (integer, ≥ 1; required) - `is_current` (boolean; required) - `created_at` (string, date-time; required) - `started_at` (string | null, date-time; required) - `completed_at` (string | null, date-time; required) - `error` (object | null; required) - `code` (string; required) - `message` (string; required) - `artifact` (object | null; required) - `asset_id` (string, uuid; required) - `filename` (string, 1–255 chars; required) - `content_type` (string, min 1 chars; required) - `size_bytes` (integer, ≥ 1; required) - `download_url` (string, uri; required) - `download_url_expires_at` (string, date-time; required) - `artifact_expires_at` (string, date-time; required) - `matches_current_authority` (boolean | null; required): Whether the latest export's document, cast, and mastering versions match. Null without an export. true does not guarantee latest takes, completion, timeline completeness, or a valid artifact. Errors: `400`, `401`, `403`, `404`, `500` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/export-freshness" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### List exports `GET /studio/exports` Scope: `studio:read` · Operation ID: `listStudioExports` Lists the account's exports, newest first. Completed exports include a download URL that is valid for twelve hours; the file itself is kept for thirty days. #### Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `section_id` | string, uuid | No | | | `format` | string, one of `mp3`, `wav`, `m4b`, `epub`, `srt` | No | | | `status` | string, one of `queued`, `running`, `retry_wait`, `canceling`, `completed`, `failed`, `cancelled`, `superseded` | No | | | `cursor` | integer, ≥ 0, default 0 | No | | | `limit` | integer, 1–100, default 25 | No | | #### Response 200 One export page. Fields inside `data`: - `schema_version` (1; required) - `exports` (array of object; required) - `schema_version` (1; required) - `id` (string, uuid; required) - `project_id` (string, uuid; required) - `section_id` (string, uuid; required) - `format` (string, one of `mp3`, `wav`, `m4b`, `epub`, `srt`; required) - `status` (string, one of `queued`, `running`, `retry_wait`, `canceling`, `completed`, `failed`, `cancelled`, `superseded`; required) - `phase` (string, one of `queued`, `compiling`, `claiming_artifact`, `downloading`, `mixing`, `encoding`, `packaging`, `uploading`, `complete`, `failed`, `cancelled`; required) - `percent` (integer, 0–100; required) - `cache_result` (string, one of `miss`, `reused`, `coalesced`; required) - `authority` (object; required) - `document_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `mastering_version` (integer, ≥ 1; required) - `is_current` (boolean; required) - `created_at` (string, date-time; required) - `started_at` (string | null, date-time; required) - `completed_at` (string | null, date-time; required) - `error` (object | null; required) - `code` (string; required) - `message` (string; required) - `artifact` (object | null; required) - `asset_id` (string, uuid; required) - `filename` (string, 1–255 chars; required) - `content_type` (string, min 1 chars; required) - `size_bytes` (integer, ≥ 1; required) - `download_url` (string, uri; required) - `download_url_expires_at` (string, date-time; required) - `artifact_expires_at` (string, date-time; required) - `next_cursor` (string | null, min 1 chars; required) Errors: `400`, `401`, `403`, `404`, `409`, `500`, `503` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/exports" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Create an export `POST /studio/exports` Scope: `studio:write` · Retries: send an `Idempotency-Key` header · Operation ID: `createStudioExport` Renders one section into `mp3`, `wav`, `m4b`, synchronized `epub`, or `srt`, pinned to the document, speaker registry, and mastering versions you send. Returns `202` for new work or `200` when an identical, unexpired file is reused. Requires an `Idempotency-Key` header. #### Headers | Name | Type | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | string, 1–255 chars | Yes | Opaque caller key. The same key and request replay safely; the same key with a different request conflicts. | #### Request body (application/json, required) - `schema_version` (1; required) - `section_id` (string, uuid; required) - `document_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `mastering_version` (integer, ≥ 1; required) - `format` (string, one of `mp3`, `wav`, `m4b`, `epub`, `srt`; required) #### Response 200 An unexpired identical artifact was reused. Fields inside `data`: - `schema_version` (1; required) - `id` (string, uuid; required) - `project_id` (string, uuid; required) - `section_id` (string, uuid; required) - `format` (string, one of `mp3`, `wav`, `m4b`, `epub`, `srt`; required) - `status` (string, one of `queued`, `running`, `retry_wait`, `canceling`, `completed`, `failed`, `cancelled`, `superseded`; required) - `phase` (string, one of `queued`, `compiling`, `claiming_artifact`, `downloading`, `mixing`, `encoding`, `packaging`, `uploading`, `complete`, `failed`, `cancelled`; required) - `percent` (integer, 0–100; required) - `cache_result` (string, one of `miss`, `reused`, `coalesced`; required) - `authority` (object; required) - `document_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `mastering_version` (integer, ≥ 1; required) - `is_current` (boolean; required) - `created_at` (string, date-time; required) - `started_at` (string | null, date-time; required) - `completed_at` (string | null, date-time; required) - `error` (object | null; required) - `code` (string; required) - `message` (string; required) - `artifact` (object | null; required) - `asset_id` (string, uuid; required) - `filename` (string, 1–255 chars; required) - `content_type` (string, min 1 chars; required) - `size_bytes` (integer, ≥ 1; required) - `download_url` (string, uri; required) - `download_url_expires_at` (string, date-time; required) - `artifact_expires_at` (string, date-time; required) #### Response 202 New durable export work accepted. Fields inside `data`: - `schema_version` (1; required) - `id` (string, uuid; required) - `project_id` (string, uuid; required) - `section_id` (string, uuid; required) - `format` (string, one of `mp3`, `wav`, `m4b`, `epub`, `srt`; required) - `status` (string, one of `queued`, `running`, `retry_wait`, `canceling`, `completed`, `failed`, `cancelled`, `superseded`; required) - `phase` (string, one of `queued`, `compiling`, `claiming_artifact`, `downloading`, `mixing`, `encoding`, `packaging`, `uploading`, `complete`, `failed`, `cancelled`; required) - `percent` (integer, 0–100; required) - `cache_result` (string, one of `miss`, `reused`, `coalesced`; required) - `authority` (object; required) - `document_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `mastering_version` (integer, ≥ 1; required) - `is_current` (boolean; required) - `created_at` (string, date-time; required) - `started_at` (string | null, date-time; required) - `completed_at` (string | null, date-time; required) - `error` (object | null; required) - `code` (string; required) - `message` (string; required) - `artifact` (object | null; required) - `asset_id` (string, uuid; required) - `filename` (string, 1–255 chars; required) - `content_type` (string, min 1 chars; required) - `size_bytes` (integer, ≥ 1; required) - `download_url` (string, uri; required) - `download_url_expires_at` (string, date-time; required) - `artifact_expires_at` (string, date-time; required) Errors: `400`, `401`, `403`, `404`, `409`, `500`, `502`, `503` return the standard error envelope. #### Example request ```bash curl -X POST "https://lyricwinter.com/api/v1/studio/exports" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "schema_version": 1, "section_id": "11111111-1111-4111-8111-111111111111", "document_version": 7, "speaker_registry_version": 3, "mastering_version": 4, "format": "mp3" }' ``` ### Get an export `GET /studio/exports/{exportId}` Scope: `studio:read` · Operation ID: `getStudioExport` Returns an export's status, phase, percent complete, and its completed, unexpired downloadable `artifact`. Historical files remain tied to their original export. `authority.is_current` compares document, cast, and mastering versions only, not the latest audio rendition. For timings matching this file, fetch `/studio/exports/{exportId}/manifest`. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `exportId` | string, uuid | Yes | | #### Response 200 Current durable export state. Fields inside `data`: - `schema_version` (1; required) - `id` (string, uuid; required) - `project_id` (string, uuid; required) - `section_id` (string, uuid; required) - `format` (string, one of `mp3`, `wav`, `m4b`, `epub`, `srt`; required) - `status` (string, one of `queued`, `running`, `retry_wait`, `canceling`, `completed`, `failed`, `cancelled`, `superseded`; required) - `phase` (string, one of `queued`, `compiling`, `claiming_artifact`, `downloading`, `mixing`, `encoding`, `packaging`, `uploading`, `complete`, `failed`, `cancelled`; required) - `percent` (integer, 0–100; required) - `cache_result` (string, one of `miss`, `reused`, `coalesced`; required) - `authority` (object; required) - `document_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `mastering_version` (integer, ≥ 1; required) - `is_current` (boolean; required) - `created_at` (string, date-time; required) - `started_at` (string | null, date-time; required) - `completed_at` (string | null, date-time; required) - `error` (object | null; required) - `code` (string; required) - `message` (string; required) - `artifact` (object | null; required) - `asset_id` (string, uuid; required) - `filename` (string, 1–255 chars; required) - `content_type` (string, min 1 chars; required) - `size_bytes` (integer, ≥ 1; required) - `download_url` (string, uri; required) - `download_url_expires_at` (string, date-time; required) - `artifact_expires_at` (string, date-time; required) Errors: `400`, `401`, `403`, `404`, `409`, `500`, `503` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/exports/$EXPORT_ID" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Get an export's immutable timeline `GET /studio/exports/{exportId}/manifest` Scope: `studio:read` · Operation ID: `getStudioExportManifest` Returns the exact pinned manifest identity (`id`, content `hash`, and `schema_version`), total duration, completeness, and block timings used by this export. The response's `timing_basis` is `media_and_intra_block_silence_excluding_inter_block_spacing`: speech and narration block durations sum their contiguous playback asset and separately assembled silence units, but exclude inter-block spacing units. An asset unit can share a block's `start_ms` yet have a shorter `duration_ms` when a trailing pause follows it; do not normalize either duration. Join block-level records by stable `block_id` and word sidecars by `unit_id`, not by start time or `block_index`. `block_index` is zero-based in the captured document's full block order. Optional `schema_version=2` adds section-relative provider word timing sidecars for the pinned immutable assets, not today's retakes; missing alignment is explicitly unavailable. Default V1 is unchanged. Returns `409` before a manifest is pinned. This read does not compile or generate media. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `exportId` | string, uuid | Yes | | #### Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `schema_version` | integer, default 1, one of `1`, `2` | No | | #### Response 200 The export's pinned block timeline and content-addressed manifest identity. Fields inside `data`: - One of: - **schema_version: 1, timing_basis: "media_and_intra_block_silence_excluding_inter_block_spacing"** - `schema_version` (1; required) - `export_id` (string, uuid; required) - `project_id` (string, uuid; required) - `section_id` (string, uuid; required) - `manifest` (object; required) - `id` (string, uuid; required) - `hash` (string, pattern ^[a-f0-9]{64}$; required): Content hash of the exact immutable render manifest used by the export. - `schema_version` (string, one of `studio-render-manifest-1`, `studio-render-manifest-2`; required) - `source_authority` (object; required) - `document_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `mastering_version` (integer, ≥ 1; required) - `duration_ms` (integer, ≥ 0; required) - `completeness` (string, one of `partial`, `complete`; required) - `timing_basis` ("media_and_intra_block_silence_excluding_inter_block_spacing"; required) - `blocks` (array of object; required) - `block_id` (string, uuid; required) - `block_index` (integer, ≥ 0; required): Zero-based position in the export's captured document block order, including non-speech blocks. - `media_kind` (string, one of `speech`, `narration`, `sfx`; required) - `start_ms` (integer, ≥ 0; required) - `duration_ms` (integer, ≥ 1; required): Block span including internal authored silence but excluding inter-block spacing; SFX spans its overlay. - **schema_version: 2, timing_basis: "media_and_intra_block_silence_excluding_inter_block_spacing"**: Opt-in export-bound timeline with word timing sidecars from the exact source assets in its immutable stored manifest, never current retakes. - `schema_version` (2; required) - `export_id` (string, uuid; required) - `project_id` (string, uuid; required) - `section_id` (string, uuid; required) - `manifest` (object; required) - `id` (string, uuid; required) - `hash` (string, pattern ^[a-f0-9]{64}$; required): Content hash of the exact immutable render manifest used by the export. - `schema_version` (string, one of `studio-render-manifest-1`, `studio-render-manifest-2`; required) - `source_authority` (object; required) - `document_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `mastering_version` (integer, ≥ 1; required) - `duration_ms` (integer, ≥ 0; required) - `completeness` (string, one of `partial`, `complete`; required) - `timing_basis` ("media_and_intra_block_silence_excluding_inter_block_spacing"; required) - `blocks` (array of object; required) - `block_id` (string, uuid; required) - `block_index` (integer, ≥ 0; required): Zero-based position in the export's captured document block order, including non-speech blocks. - `media_kind` (string, one of `speech`, `narration`, `sfx`; required) - `start_ms` (integer, ≥ 0; required) - `duration_ms` (integer, ≥ 1; required): Block span including internal authored silence but excluding inter-block spacing; SFX spans its overlay. - `unit_word_timings` (array of object, max 60000 items; required) - One of: - **word_timing: "provider"** - `unit_id` (string, uuid; required) - `block_id` (string, uuid; required) - `asset_id` (string, uuid; required) - `word_timing` ("provider"; required) - `words` (array of object, 1–10000 items; required) - `text` (string, 1–4096 chars; required) - `start_ms` (integer, ≥ 0; required) - `end_ms` (integer, ≥ 0; required) - **word_timing: "unavailable"** - `unit_id` (string, uuid; required) - `block_id` (string, uuid; required) - `asset_id` (string, uuid; required) - `word_timing` ("unavailable"; required) Errors: `400`, `401`, `403`, `404`, `409`, `500` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/exports/$EXPORT_ID/manifest" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Cancel an export `POST /studio/exports/{exportId}/cancel` Scope: `studio:write` · Operation ID: `cancelStudioExport` Cancels a queued export immediately or asks a running one to stop. Files already produced for other requests are not deleted. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `exportId` | string, uuid | Yes | | #### Response 200 Current cancelled or canceling export state. Fields inside `data`: - `schema_version` (1; required) - `id` (string, uuid; required) - `project_id` (string, uuid; required) - `section_id` (string, uuid; required) - `format` (string, one of `mp3`, `wav`, `m4b`, `epub`, `srt`; required) - `status` (string, one of `queued`, `running`, `retry_wait`, `canceling`, `completed`, `failed`, `cancelled`, `superseded`; required) - `phase` (string, one of `queued`, `compiling`, `claiming_artifact`, `downloading`, `mixing`, `encoding`, `packaging`, `uploading`, `complete`, `failed`, `cancelled`; required) - `percent` (integer, 0–100; required) - `cache_result` (string, one of `miss`, `reused`, `coalesced`; required) - `authority` (object; required) - `document_version` (integer, ≥ 1; required) - `speaker_registry_version` (integer, ≥ 1; required) - `mastering_version` (integer, ≥ 1; required) - `is_current` (boolean; required) - `created_at` (string, date-time; required) - `started_at` (string | null, date-time; required) - `completed_at` (string | null, date-time; required) - `error` (object | null; required) - `code` (string; required) - `message` (string; required) - `artifact` (object | null; required) - `asset_id` (string, uuid; required) - `filename` (string, 1–255 chars; required) - `content_type` (string, min 1 chars; required) - `size_bytes` (integer, ≥ 1; required) - `download_url` (string, uri; required) - `download_url_expires_at` (string, date-time; required) - `artifact_expires_at` (string, date-time; required) Errors: `400`, `401`, `403`, `404`, `409`, `500` return the standard error envelope. #### Example request ```bash curl -X POST "https://lyricwinter.com/api/v1/studio/exports/$EXPORT_ID/cancel" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` --- Source: https://lyricwinter.com/docs/api/sharing.md --- title: "Sharing API" description: "Publish a section or a whole project at a stable public link, read its sharing state, or revoke the link." canonical_url: https://lyricwinter.com/docs/api/sharing markdown_url: https://lyricwinter.com/docs/api/sharing.md last_updated: 2026-09-30 status: beta --- # LyricWinter API reference: Sharing > Publish a section or a whole project at a stable public link, read its sharing state, or revoke the link. The LyricWinter API is in beta. Base URL: `https://lyricwinter.com/api/v1`. Authenticate every request with `Authorization: Bearer lw_...`. Successful responses are wrapped as `{ "data": ..., "request_id": "req_..." }`. Machine-readable contract: https://lyricwinter.com/api/v1/openapi.json. ## Endpoints - [Get section sharing](https://lyricwinter.com/docs/api/sharing#get-studio-public-share): `GET /studio/sections/{sectionId}/share` - [Share a section](https://lyricwinter.com/docs/api/sharing#ensure-studio-public-share): `POST /studio/sections/{sectionId}/share` - [Stop sharing a section](https://lyricwinter.com/docs/api/sharing#revoke-studio-public-share): `DELETE /studio/sections/{sectionId}/share` - [Get project sharing](https://lyricwinter.com/docs/api/sharing#get-studio-project-share-state): `GET /studio/projects/{projectId}/share` - [Share a project](https://lyricwinter.com/docs/api/sharing#publish-studio-project): `POST /studio/projects/{projectId}/share` - [Stop sharing a project](https://lyricwinter.com/docs/api/sharing#revoke-studio-project-share): `DELETE /studio/projects/{projectId}/share` ### Get section sharing `GET /studio/sections/{sectionId}/share` Scope: `studio:read` · Operation ID: `getStudioPublicShare` Returns whether the section is public and its active public link, if any. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `sectionId` | string, uuid | Yes | | #### Response 200 Current section sharing state. Fields inside `data`: - `schema_version` (1; required) - `section_id` (string, uuid; required) - `is_public` (boolean; required) - `share` (object | null; required): Active link; non-null exactly when is_public is true. - `id` (string, uuid; required) - `url` (string, uri; required): Public section page URL of the form /s/. - `created_at` (string, date-time; required) Errors: `400`, `401`, `403`, `404`, `500`, `503` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/sections/$SECTION_ID/share" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Share a section `POST /studio/sections/{sectionId}/share` Scope: `studio:write` · Operation ID: `ensureStudioPublicShare` Creates a stable public link for the section, or returns the existing one. The public page always plays the latest saved version. Send an empty JSON object, or name the `source` surface you shared from. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `sectionId` | string, uuid | Yes | | #### Request body (application/json, required) - `source` (string, one of `toolbar`, `playback_menu`, `library`, `completion_prompt`; optional): Studio surface the owner shared from. #### Response 201 Section published with its active link (also returned when already public). Fields inside `data`: - `schema_version` (1; required) - `section_id` (string, uuid; required) - `is_public` (boolean; required) - `share` (object | null; required): Active link; non-null exactly when is_public is true. - `id` (string, uuid; required) - `url` (string, uri; required): Public section page URL of the form /s/. - `created_at` (string, date-time; required) Errors: `400`, `401`, `403`, `404`, `409`, `500`, `503` return the standard error envelope. #### Example request ```bash curl -X POST "https://lyricwinter.com/api/v1/studio/sections/$SECTION_ID/share" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ### Stop sharing a section `DELETE /studio/sections/{sectionId}/share` Scope: `studio:write` · Operation ID: `revokeStudioPublicShare` Revokes the section's public link and makes the section private. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `sectionId` | string, uuid | Yes | | #### Response 200 Section is private and the prior link is revoked. Fields inside `data`: - `schema_version` (1; required) - `section_id` (string, uuid; required) - `is_public` (boolean; required) - `share` (object | null; required): Active link; non-null exactly when is_public is true. - `id` (string, uuid; required) - `url` (string, uri; required): Public section page URL of the form /s/. - `created_at` (string, date-time; required) Errors: `400`, `401`, `403`, `404`, `500`, `503` return the standard error envelope. #### Example request ```bash curl -X DELETE "https://lyricwinter.com/api/v1/studio/sections/$SECTION_ID/share" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Get project sharing `GET /studio/projects/{projectId}/share` Scope: `studio:read` · Operation ID: `getStudioProjectShareState` Returns whether the project has a public link and the active link, if any. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | #### Response 200 Current project public-link state. Fields inside `data`: - `schema_version` (1; required) - `project_id` (string, uuid; required) - `is_public` (boolean; required) - `share` (object | null; required): Active link; non-null exactly when is_public is true. - `id` (string, uuid; required) - `url` (string, uri; required): Public project page URL of the form /p/. - `created_at` (string, date-time; required) Errors: `400`, `401`, `403`, `404`, `500`, `503` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/share" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Share a project `POST /studio/projects/{projectId}/share` Scope: `studio:write` · Operation ID: `publishStudioProject` Creates a stable public link for the whole project, or returns the existing one. Send an empty JSON object, or name the `source` surface you shared from. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | #### Request body (application/json, required) - `source` (string, one of `toolbar`, `playback_menu`, `library`, `completion_prompt`; optional): Studio surface the owner shared from. #### Response 201 Stable project public link. Fields inside `data`: - `schema_version` (1; required) - `project_id` (string, uuid; required) - `is_public` (boolean; required) - `share` (object | null; required): Active link; non-null exactly when is_public is true. - `id` (string, uuid; required) - `url` (string, uri; required): Public project page URL of the form /p/. - `created_at` (string, date-time; required) Errors: `400`, `401`, `403`, `404`, `409`, `500`, `503` return the standard error envelope. #### Example request ```bash curl -X POST "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/share" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ### Stop sharing a project `DELETE /studio/projects/{projectId}/share` Scope: `studio:write` · Operation ID: `revokeStudioProjectShare` Revokes the project's public link. Send no request body. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | #### Response 200 Project sharing is revoked. Fields inside `data`: - `schema_version` (1; required) - `project_id` (string, uuid; required) - `is_public` (boolean; required) - `share` (object | null; required): Active link; non-null exactly when is_public is true. - `id` (string, uuid; required) - `url` (string, uri; required): Public project page URL of the form /p/. - `created_at` (string, date-time; required) Errors: `400`, `401`, `403`, `404`, `500`, `503` return the standard error envelope. #### Example request ```bash curl -X DELETE "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/share" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` --- Source: https://lyricwinter.com/docs/api/sounds.md --- title: "Sound library API" description: "Upload sound effects and ambience up to 256 MiB and 65 minutes into the Studio sound library, manage their metadata, publish them to the public catalog, and link them to projects so Create Audio can place them automatically. Processing verifies duration; the sanitized WAV can be up to 720 MiB. Pinned project sounds can play as long ambience overlays; provider-generated SFX remain limited to five minutes." canonical_url: https://lyricwinter.com/docs/api/sounds markdown_url: https://lyricwinter.com/docs/api/sounds.md last_updated: 2026-10-01 status: beta --- # LyricWinter API reference: Sound library > Upload sound effects and ambience up to 256 MiB and 65 minutes into the Studio sound library, manage their metadata, publish them to the public catalog, and link them to projects so Create Audio can place them automatically. Processing verifies duration; the sanitized WAV can be up to 720 MiB. Pinned project sounds can play as long ambience overlays; provider-generated SFX remain limited to five minutes. The LyricWinter API is in beta. Base URL: `https://lyricwinter.com/api/v1`. Authenticate every request with `Authorization: Bearer lw_...`. Successful responses are wrapped as `{ "data": ..., "request_id": "req_..." }`. Machine-readable contract: https://lyricwinter.com/api/v1/openapi.json. ## Endpoints - [List sounds](https://lyricwinter.com/docs/api/sounds#list-studio-sounds): `GET /studio/sounds` - [Start a sound upload](https://lyricwinter.com/docs/api/sounds#create-studio-sound-upload): `POST /studio/sounds` - [Complete a sound upload](https://lyricwinter.com/docs/api/sounds#complete-studio-sound-upload): `POST /studio/sounds/{soundId}/uploads/{uploadId}/complete` - [Get sound upload status](https://lyricwinter.com/docs/api/sounds#get-studio-sound-upload-status): `GET /studio/sounds/{soundId}/uploads/latest` - [Get a sound](https://lyricwinter.com/docs/api/sounds#get-studio-sound): `GET /studio/sounds/{soundId}` - [Update a sound](https://lyricwinter.com/docs/api/sounds#update-studio-sound): `PATCH /studio/sounds/{soundId}` - [Delete a sound](https://lyricwinter.com/docs/api/sounds#delete-studio-sound): `DELETE /studio/sounds/{soundId}` - [Create a sound preview URL](https://lyricwinter.com/docs/api/sounds#create-studio-sound-preview-url): `POST /studio/sounds/{soundId}/preview-url` - [List project sounds](https://lyricwinter.com/docs/api/sounds#list-studio-project-sounds): `GET /studio/projects/{projectId}/sounds` - [Link a sound to a project](https://lyricwinter.com/docs/api/sounds#add-studio-project-sound): `POST /studio/projects/{projectId}/sounds` - [Unlink a sound from a project](https://lyricwinter.com/docs/api/sounds#remove-studio-project-sound): `DELETE /studio/projects/{projectId}/sounds/{soundId}` - [List public sounds](https://lyricwinter.com/docs/api/sounds#list-public-sounds): `GET /sounds` - [Get a public sound](https://lyricwinter.com/docs/api/sounds#get-public-sound): `GET /sounds/{soundId}` ### List sounds `GET /studio/sounds` Scope: `studio:read` · Operation ID: `listStudioSounds` Lists sounds in your library, sounds shared with you, or public sounds, filtered by `scope` and an optional search query. Results are cursor-paginated. #### Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `scope` | string, default "mine", one of `mine`, `shared`, `public` | No | | | `q` | string, max 200 chars | No | | | `project_id` | string, uuid | No | | | `in_project` | boolean | No | | | `limit` | integer, 1–100, default 30 | No | | | `cursor` | string, max 500 chars | No | | #### Response 200 Cursor-paginated sound library. Fields inside `data`: - `schema_version` ("studio-sound-library-1"; required) - `sounds` (array of object, max 100 items; required) - `id` (string, uuid; required) - `name` (string, 1–120 chars; required) - `description` (string, 1–1000 chars; required) - `tags` (array of string, 1–12 items, unique; required) - `status` (string, one of `uploading`, `processing`, `ready`, `failed`; required) - `is_public` (boolean; required) - `owner_display_name` (string | null, 1–200 chars; required) - `is_owned_by_user` (boolean; required) - `is_shared_with_user` (boolean; required) - `incoming_share` (object | null; required) - `id` (string, uuid; required) - `status` (string, one of `pending`, `accepted`, `declined`; required) - `is_in_project` (boolean; required) - `can_edit` (boolean; required) - `can_delete` (boolean; required) - `can_share` (boolean; required) - `metadata_version` (integer, ≥ 1; required) - `current_revision` (object | null; required) - `id` (string, uuid; required) - `revision_number` (integer, ≥ 1; required) - `duration_ms` (integer, 100–3900000; required) - `mime_type` (string, 1–200 chars; required) - `file_size_bytes` (integer, 1–754974720; required) - `created_at` (string, date-time; required) - `error` (object | null; required) - `code` (string | null, max 200 chars; required) - `message` (string, 1–1000 chars; required) - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) - `next_cursor` (string | null, 1–500 chars; required) Errors: `default` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/sounds" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Start a sound upload `POST /studio/sounds` Scope: `studio:write` · Retries: reuse the same `client_mutation_id` · Operation ID: `createStudioSoundUpload` Creates a sound and returns a signed upload session. Files accept up to 256 MiB; decoded audio must be at most 65 minutes. Upload the file to the session URL, then call the complete endpoint. Processing verifies duration and publishes it as `current_revision.duration_ms`; client-supplied duration is not accepted as authority. Set `is_public: true` to list the sound in the public catalog and on [lyricwinter.com/sounds](/sounds) once it is ready. #### Request body (application/json, required) - `schema_version` ("studio-sound-upload-1"; required) - `client_mutation_id` (string, uuid; required) - `name` (string, 1–120 chars; required) - `description` (string, 1–1000 chars; required) - `tags` (array of string, 1–12 items, unique; required) - `is_public` (boolean; required) - `rights_acknowledgment_version` ("studio-sound-rights-1"; required) - `file` (object; required): Upload files up to 268435456 bytes (256 MiB) and 3900000 ms (65 minutes). Duration is verified from decoded audio during processing, not trusted from a client declaration. The sanitized revision reports its verified duration_ms. - `filename` (string, 1–255 chars; required) - `content_type` (string, one of `audio/wav`, `audio/x-wav`, `audio/mpeg`, `audio/ogg`, `audio/opus`, `audio/flac`, `audio/mp4`, `audio/aac`, `audio/webm`; required) - `size_bytes` (integer, 1–268435456; required) #### Response 201 Upload session created. Fields inside `data`: - `schema_version` ("studio-sound-upload-1"; required) - `sound` (object; required) - `id` (string, uuid; required) - `name` (string, 1–120 chars; required) - `description` (string, 1–1000 chars; required) - `tags` (array of string, 1–12 items, unique; required) - `status` (string, one of `uploading`, `processing`, `ready`, `failed`; required) - `is_public` (boolean; required) - `owner_display_name` (string | null, 1–200 chars; required) - `is_owned_by_user` (boolean; required) - `is_shared_with_user` (boolean; required) - `incoming_share` (object | null; required) - `id` (string, uuid; required) - `status` (string, one of `pending`, `accepted`, `declined`; required) - `is_in_project` (boolean; required) - `can_edit` (boolean; required) - `can_delete` (boolean; required) - `can_share` (boolean; required) - `metadata_version` (integer, ≥ 1; required) - `current_revision` (object | null; required) - `id` (string, uuid; required) - `revision_number` (integer, ≥ 1; required) - `duration_ms` (integer, 100–3900000; required) - `mime_type` (string, 1–200 chars; required) - `file_size_bytes` (integer, 1–754974720; required) - `created_at` (string, date-time; required) - `error` (object | null; required) - `code` (string | null, max 200 chars; required) - `message` (string, 1–1000 chars; required) - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) - `upload` (object; required) - `id` (string, uuid; required) - `url` (string, uri; required) - `method` ("PUT"; required) - `headers` (map of string; required) - `expires_at` (string, date-time; required) Errors: `default` return the standard error envelope. #### Example request ```bash curl -X POST "https://lyricwinter.com/api/v1/studio/sounds" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schema_version": "studio-sound-upload-1", "client_mutation_id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10", "name": "My integration", "description": "string", "tags": [ "string" ], "is_public": true, "rights_acknowledgment_version": "studio-sound-rights-1", "file": { "filename": "string", "content_type": "audio/wav", "size_bytes": 1 } }' ``` ### Complete a sound upload `POST /studio/sounds/{soundId}/uploads/{uploadId}/complete` Scope: `studio:write` · Operation ID: `completeStudioSoundUpload` Tells LyricWinter the file has been uploaded so it can process the sound. Poll the latest upload status until the sound is ready. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `soundId` | string, uuid | Yes | | | `uploadId` | string, uuid | Yes | | #### Request body (application/json, required) - `schema_version` ("studio-sound-upload-complete-1"; required) #### Response 200 Idempotent finalization result. Fields inside `data`: - `id` (string, uuid; required) - `name` (string, 1–120 chars; required) - `description` (string, 1–1000 chars; required) - `tags` (array of string, 1–12 items, unique; required) - `status` (string, one of `uploading`, `processing`, `ready`, `failed`; required) - `is_public` (boolean; required) - `owner_display_name` (string | null, 1–200 chars; required) - `is_owned_by_user` (boolean; required) - `is_shared_with_user` (boolean; required) - `incoming_share` (object | null; required) - `id` (string, uuid; required) - `status` (string, one of `pending`, `accepted`, `declined`; required) - `is_in_project` (boolean; required) - `can_edit` (boolean; required) - `can_delete` (boolean; required) - `can_share` (boolean; required) - `metadata_version` (integer, ≥ 1; required) - `current_revision` (object | null; required) - `id` (string, uuid; required) - `revision_number` (integer, ≥ 1; required) - `duration_ms` (integer, 100–3900000; required) - `mime_type` (string, 1–200 chars; required) - `file_size_bytes` (integer, 1–754974720; required) - `created_at` (string, date-time; required) - `error` (object | null; required) - `code` (string | null, max 200 chars; required) - `message` (string, 1–1000 chars; required) - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) #### Response 202 Sanitization accepted. Fields inside `data`: - `id` (string, uuid; required) - `name` (string, 1–120 chars; required) - `description` (string, 1–1000 chars; required) - `tags` (array of string, 1–12 items, unique; required) - `status` (string, one of `uploading`, `processing`, `ready`, `failed`; required) - `is_public` (boolean; required) - `owner_display_name` (string | null, 1–200 chars; required) - `is_owned_by_user` (boolean; required) - `is_shared_with_user` (boolean; required) - `incoming_share` (object | null; required) - `id` (string, uuid; required) - `status` (string, one of `pending`, `accepted`, `declined`; required) - `is_in_project` (boolean; required) - `can_edit` (boolean; required) - `can_delete` (boolean; required) - `can_share` (boolean; required) - `metadata_version` (integer, ≥ 1; required) - `current_revision` (object | null; required) - `id` (string, uuid; required) - `revision_number` (integer, ≥ 1; required) - `duration_ms` (integer, 100–3900000; required) - `mime_type` (string, 1–200 chars; required) - `file_size_bytes` (integer, 1–754974720; required) - `created_at` (string, date-time; required) - `error` (object | null; required) - `code` (string | null, max 200 chars; required) - `message` (string, 1–1000 chars; required) - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) Errors: `default` return the standard error envelope. #### Example request ```bash curl -X POST "https://lyricwinter.com/api/v1/studio/sounds/$SOUND_ID/uploads/$UPLOAD_ID/complete" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schema_version": "studio-sound-upload-complete-1" }' ``` ### Get sound upload status `GET /studio/sounds/{soundId}/uploads/latest` Scope: `studio:read` · Operation ID: `getStudioSoundUploadStatus` Returns the processing status of the most recent upload for a sound you own. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `soundId` | string, uuid | Yes | | #### Response 200 Latest owner upload, independent of the playable revision; null when none exists. Fields inside `data`: - `schema_version` (string, one of `studio-sound-upload-status-1`; required) - `sound_id` (string, uuid; required) - `latest` (object | null; required) - `id` (string, uuid; required) - `revision_number` (integer, ≥ 1; required) - `is_replacement` (boolean; required) - `status` (string, one of `pending_upload`, `processing`, `ready`, `failed`, `expired`; required) - `completed_revision_id` (string | null, uuid; required) - `error` (object | null; required) - `code` (string | null; required) - `message` (string; required) - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) - `expires_at` (string, date-time; required) Errors: `default` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/sounds/$SOUND_ID/uploads/latest" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Get a sound `GET /studio/sounds/{soundId}` Scope: `studio:read` · Operation ID: `getStudioSound` Returns one sound's metadata and processing state. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `soundId` | string, uuid | Yes | | #### Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | string, uuid | No | | #### Response 200 Accessible sound detail. Fields inside `data`: - `id` (string, uuid; required) - `name` (string, 1–120 chars; required) - `description` (string, 1–1000 chars; required) - `tags` (array of string, 1–12 items, unique; required) - `status` (string, one of `uploading`, `processing`, `ready`, `failed`; required) - `is_public` (boolean; required) - `owner_display_name` (string | null, 1–200 chars; required) - `is_owned_by_user` (boolean; required) - `is_shared_with_user` (boolean; required) - `incoming_share` (object | null; required) - `id` (string, uuid; required) - `status` (string, one of `pending`, `accepted`, `declined`; required) - `is_in_project` (boolean; required) - `can_edit` (boolean; required) - `can_delete` (boolean; required) - `can_share` (boolean; required) - `metadata_version` (integer, ≥ 1; required) - `current_revision` (object | null; required) - `id` (string, uuid; required) - `revision_number` (integer, ≥ 1; required) - `duration_ms` (integer, 100–3900000; required) - `mime_type` (string, 1–200 chars; required) - `file_size_bytes` (integer, 1–754974720; required) - `created_at` (string, date-time; required) - `error` (object | null; required) - `code` (string | null, max 200 chars; required) - `message` (string, 1–1000 chars; required) - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) Errors: `default` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/sounds/$SOUND_ID" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Update a sound `PATCH /studio/sounds/{soundId}` Scope: `studio:write` · Operation ID: `updateStudioSound` Updates a sound's name, description, tags, or `is_public` visibility. Publishing a sound that is still processing returns `409`. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `soundId` | string, uuid | Yes | | #### Request body (application/json, required) - any #### Response 200 Updated sound. Fields inside `data`: - `id` (string, uuid; required) - `name` (string, 1–120 chars; required) - `description` (string, 1–1000 chars; required) - `tags` (array of string, 1–12 items, unique; required) - `status` (string, one of `uploading`, `processing`, `ready`, `failed`; required) - `is_public` (boolean; required) - `owner_display_name` (string | null, 1–200 chars; required) - `is_owned_by_user` (boolean; required) - `is_shared_with_user` (boolean; required) - `incoming_share` (object | null; required) - `id` (string, uuid; required) - `status` (string, one of `pending`, `accepted`, `declined`; required) - `is_in_project` (boolean; required) - `can_edit` (boolean; required) - `can_delete` (boolean; required) - `can_share` (boolean; required) - `metadata_version` (integer, ≥ 1; required) - `current_revision` (object | null; required) - `id` (string, uuid; required) - `revision_number` (integer, ≥ 1; required) - `duration_ms` (integer, 100–3900000; required) - `mime_type` (string, 1–200 chars; required) - `file_size_bytes` (integer, 1–754974720; required) - `created_at` (string, date-time; required) - `error` (object | null; required) - `code` (string | null, max 200 chars; required) - `message` (string, 1–1000 chars; required) - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) Errors: `default` return the standard error envelope. #### Example request ```bash curl -X PATCH "https://lyricwinter.com/api/v1/studio/sounds/$SOUND_ID" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d 'null' ``` ### Delete a sound `DELETE /studio/sounds/{soundId}` Scope: `studio:write` · Retries: reuse the same `client_mutation_id` · Operation ID: `deleteStudioSound` Deletes a sound you own. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `soundId` | string, uuid | Yes | | #### Request body (application/json, required) - `schema_version` ("studio-sound-mutation-1"; required) - `client_mutation_id` (string, uuid; required) - `expected_metadata_version` (integer, ≥ 1; required) #### Response 200 Logical sound deleted. Fields inside `data`: - `sound_effect_id` (string, uuid; required) - `deleted` (true; required) Errors: `default` return the standard error envelope. #### Example request ```bash curl -X DELETE "https://lyricwinter.com/api/v1/studio/sounds/$SOUND_ID" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schema_version": "studio-sound-mutation-1", "client_mutation_id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10", "expected_metadata_version": 1 }' ``` ### Create a sound preview URL `POST /studio/sounds/{soundId}/preview-url` Scope: `studio:read` · Operation ID: `createStudioSoundPreviewUrl` Returns a short-lived URL for listening to a sound. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `soundId` | string, uuid | Yes | | #### Response 200 Access-checked short-lived same-origin preview capability URL. Fields inside `data`: - `schema_version` ("studio-sound-preview-1"; required) - `url` (string, uri; required) - `expires_at` (string, date-time; required) Errors: `default` return the standard error envelope. #### Example request ```bash curl -X POST "https://lyricwinter.com/api/v1/studio/sounds/$SOUND_ID/preview-url" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### List project sounds `GET /studio/projects/{projectId}/sounds` Scope: `studio:read` · Operation ID: `listStudioProjectSounds` Lists the sounds linked to a project. Create Audio can place linked sounds automatically when `place_project_sounds` is on. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | #### Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `q` | string, max 200 chars | No | | | `limit` | integer, 1–100, default 50 | No | | | `cursor` | string, max 500 chars | No | | #### Response 200 Project sound registry page. Fields inside `data`: - `schema_version` ("studio-sound-library-1"; required) - `sounds` (array of object, max 100 items; required) - `id` (string, uuid; required) - `name` (string, 1–120 chars; required) - `description` (string, 1–1000 chars; required) - `tags` (array of string, 1–12 items, unique; required) - `status` (string, one of `uploading`, `processing`, `ready`, `failed`; required) - `is_public` (boolean; required) - `owner_display_name` (string | null, 1–200 chars; required) - `is_owned_by_user` (boolean; required) - `is_shared_with_user` (boolean; required) - `incoming_share` (object | null; required) - `id` (string, uuid; required) - `status` (string, one of `pending`, `accepted`, `declined`; required) - `is_in_project` (boolean; required) - `can_edit` (boolean; required) - `can_delete` (boolean; required) - `can_share` (boolean; required) - `metadata_version` (integer, ≥ 1; required) - `current_revision` (object | null; required) - `id` (string, uuid; required) - `revision_number` (integer, ≥ 1; required) - `duration_ms` (integer, 100–3900000; required) - `mime_type` (string, 1–200 chars; required) - `file_size_bytes` (integer, 1–754974720; required) - `created_at` (string, date-time; required) - `error` (object | null; required) - `code` (string | null, max 200 chars; required) - `message` (string, 1–1000 chars; required) - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) - `next_cursor` (string | null, 1–500 chars; required) - `sound_source_registry_version` (integer, ≥ 1; required) Errors: `default` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/sounds" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Link a sound to a project `POST /studio/projects/{projectId}/sounds` Scope: `studio:write` · Retries: reuse the same `client_mutation_id` · Operation ID: `addStudioProjectSound` Links a ready sound from your library to a project so Create Audio can place it. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | #### Request body (application/json, required) - `schema_version` ("studio-project-sound-source-1"; required) - `client_mutation_id` (string, uuid; required) - `sound_effect_id` (string, uuid; required) - `expected_registry_version` (integer, ≥ 1; required) #### Response 201 Project sound membership updated. Fields inside `data`: - `schema_version` ("studio-project-sound-source-1"; required) - `project_id` (string, uuid; required) - `sound_effect_id` (string, uuid; required) - `linked` (boolean; required) - `sound_source_registry_version` (integer, ≥ 1; required) Errors: `default` return the standard error envelope. #### Example request ```bash curl -X POST "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/sounds" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schema_version": "studio-project-sound-source-1", "client_mutation_id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10", "sound_effect_id": "c9f0f895-fb98-4b91-9e3f-1d2a6b7c8d20", "expected_registry_version": 1 }' ``` ### Unlink a sound from a project `DELETE /studio/projects/{projectId}/sounds/{soundId}` Scope: `studio:write` · Retries: reuse the same `client_mutation_id` · Operation ID: `removeStudioProjectSound` Removes a sound from a project. The sound stays in your library. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `projectId` | string, uuid | Yes | | | `soundId` | string, uuid | Yes | | #### Request body (application/json, required) - `schema_version` ("studio-project-sound-source-1"; required) - `client_mutation_id` (string, uuid; required) - `sound_effect_id` (string, uuid; required) - `expected_registry_version` (integer, ≥ 1; required) #### Response 200 Project sound membership updated. Fields inside `data`: - `schema_version` ("studio-project-sound-source-1"; required) - `project_id` (string, uuid; required) - `sound_effect_id` (string, uuid; required) - `linked` (boolean; required) - `sound_source_registry_version` (integer, ≥ 1; required) Errors: `default` return the standard error envelope. #### Example request ```bash curl -X DELETE "https://lyricwinter.com/api/v1/studio/projects/$PROJECT_ID/sounds/$SOUND_ID" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schema_version": "studio-project-sound-source-1", "client_mutation_id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10", "sound_effect_id": "c9f0f895-fb98-4b91-9e3f-1d2a6b7c8d20", "expected_registry_version": 1 }' ``` ### List public sounds `GET /sounds` Scope: `studio:read` · Operation ID: `listPublicSounds` Lists published sounds from the public catalog shown on [lyricwinter.com/sounds](/sounds). No sign-in is required. Filter with `q`, `tag`, or `category`, and page with `cursor`. Each sound includes its page URL and a preview URL valid for 12 hours. #### Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `q` | string, max 200 chars | No | | | `tag` | string, 1–40 chars | No | | | `category` | string, pattern ^[a-z0-9_-]{1,80}$ | No | | | `limit` | integer, 1–100, default 30 | No | | | `cursor` | string, pattern ^[0-9]{1,6}$ | No | | #### Response 200 One page of public sounds. Fields inside `data`: - `schema_version` ("public-sound-catalog-1"; required) - `sounds` (array of object, max 100 items; required) - `id` (string, uuid; required) - `name` (string, 1–120 chars; required) - `description` (string, 1–1000 chars; required) - `tags` (array of string, 1–12 items; required) - `category` (string; required): Catalog slug derived from the first tag. - `duration_ms` (integer, 100–3900000; required) - `page_url` (string, uri; required) - `preview` (object; required) - `url` (string, uri; required): Signed-out WAV preview that supports HTTP Range requests. - `expires_at` (string, date-time; required) - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) - `total` (integer, ≥ 0; required) Errors: `default` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/sounds" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` ### Get a public sound `GET /sounds/{soundId}` Scope: `studio:read` · Operation ID: `getPublicSound` Returns one published sound from the public catalog, with its page URL and a preview URL. Sounds that are private or still processing return `404`. #### Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `soundId` | string, uuid | Yes | | #### Response 200 Public sound. Fields inside `data`: - `id` (string, uuid; required) - `name` (string, 1–120 chars; required) - `description` (string, 1–1000 chars; required) - `tags` (array of string, 1–12 items; required) - `category` (string; required): Catalog slug derived from the first tag. - `duration_ms` (integer, 100–3900000; required) - `page_url` (string, uri; required) - `preview` (object; required) - `url` (string, uri; required): Signed-out WAV preview that supports HTTP Range requests. - `expires_at` (string, date-time; required) - `created_at` (string, date-time; required) - `updated_at` (string, date-time; required) Errors: `default` return the standard error envelope. #### Example request ```bash curl "https://lyricwinter.com/api/v1/sounds/$SOUND_ID" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" ``` --- Source: https://lyricwinter.com/docs/api/feedback.md --- title: "Feedback API" description: "Send product feedback with screenshots and other images so our team can see what you mean." canonical_url: https://lyricwinter.com/docs/api/feedback markdown_url: https://lyricwinter.com/docs/api/feedback.md last_updated: 2026-10-08 status: beta --- # LyricWinter API reference: Feedback > Send product feedback with screenshots and other images so our team can see what you mean. The LyricWinter API is in beta. Base URL: `https://lyricwinter.com/api/v1`. Authenticate every request with `Authorization: Bearer lw_...`. Successful responses are wrapped as `{ "data": ..., "request_id": "req_..." }`. Machine-readable contract: https://lyricwinter.com/api/v1/openapi.json. ## Endpoints - [Send feedback and images](https://lyricwinter.com/docs/api/feedback#submit-feedback): `POST /feedback` ### Send feedback and images `POST /feedback` Scope: `feedback:write` · Operation ID: `submitFeedback` Send feedback text as JSON, or use multipart/form-data to attach up to five PNG, JPEG, or WebP images by repeating the images field. Each image can be up to 5 MiB, with 15 MiB total and 32 megapixels per image. Screenshots can include circles or other annotations. Images are stored privately with your feedback. Sign in to use your account email, or supply replyEmail so our team can reply. #### Request body (application/json, required) - `replyEmail` (string, email, max 254 chars; optional): Required when authentication does not provide an account email. Trimmed and stored privately; never added to Discord notifications. Authenticated account email takes precedence. - `message` (string, 1–2000 chars; required): Feedback text. The service trims whitespace before storage and Discord forwarding. - `type` (string, default "general", one of `bug`, `feature`, `general`, `improvement`; optional) - `source` (string, 2–64 chars, pattern ^[a-z][a-z0-9_]*$, default "global"; optional): Stable identifier for the originating product surface and client. #### Response 201 Feedback was stored. Discord forwarding is best-effort and not reflected in this response. Fields inside `data`: - `success` (true; required) - `message` (string; required) - `id` (string; required): Inserted feedback row id. Errors: `400`, `401`, `429`, `500`, `503` return the standard error envelope. #### Example request ```bash curl -X POST "https://lyricwinter.com/api/v1/feedback" \ -H "Authorization: Bearer $LYRICWINTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "string" }' ``` --- Source: https://lyricwinter.com/docs/mcp.md --- title: "LyricWinter MCP server: connect Claude, ChatGPT, Cursor, and other agents" description: "Connect any MCP client to the remote LyricWinter Studio MCP server at https://lyricwinter.com/mcp using Streamable HTTP with OAuth 2.1 or a scoped API key." canonical_url: https://lyricwinter.com/docs/mcp markdown_url: https://lyricwinter.com/docs/mcp.md last_updated: 2026-10-02 status: beta --- # LyricWinter MCP server: connect Claude, ChatGPT, Cursor, and other agents > Connect any MCP client to the remote LyricWinter Studio MCP server at https://lyricwinter.com/mcp using Streamable HTTP with OAuth 2.1 or a scoped API key. The LyricWinter MCP server lets AI agents work in LyricWinter Studio on your behalf. Connect any Model Context Protocol client to `https://lyricwinter.com/mcp` and the agent can turn a story into fully cast audio, play it, share it, change character voices, add your own voice, and work with your Studio projects. In ChatGPT, Claude, and other apps that support interactive MCP Apps, a story player and a voice panel appear right in the conversation. The server uses the Streamable HTTP transport and authenticates with OAuth 2.1 or a Studio-scoped API key. | Property | Value | | --- | --- | | Server URL | `https://lyricwinter.com/mcp` | | Transport | Streamable HTTP | | Authentication | OAuth 2.1 with PKCE, or `Authorization: Bearer lw_...` | | Scopes | `studio:read` (required), `studio:write` (to create stories and audio), `voices:read` and `voices:write` (to recast characters and add your voice) | | Tools | [22 tools](/docs/mcp-tools) | | Status | Beta | ## Connect Claude **Claude on the web and Claude Desktop.** Open **Settings → Connectors**, choose **Add custom connector**, and enter `https://lyricwinter.com/mcp`. Claude opens LyricWinter so you can sign in and approve access. **Claude Code.** Add the server, then run `/mcp` inside Claude Code and choose **Authenticate**: ```bash claude mcp add --transport http lyricwinter https://lyricwinter.com/mcp ``` ## Connect ChatGPT In ChatGPT, turn on developer mode for apps and connectors, then create a connector with the URL `https://lyricwinter.com/mcp` and OAuth authentication. ChatGPT sends you to LyricWinter to sign in and approve access. Refresh the connector after LyricWinter adds new tools. ## Connect VS Code Add the server to `.vscode/mcp.json` in your workspace, or to your user MCP configuration. VS Code prompts you to sign in when the server starts. ```json title=".vscode/mcp.json" { "servers": { "lyricwinter": { "type": "http", "url": "https://lyricwinter.com/mcp" } } } ``` ## Connect Cursor Add the server to `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project). Use a Studio-scoped [API key](/docs/authentication) in the `Authorization` header: ```json title="~/.cursor/mcp.json" { "mcpServers": { "lyricwinter": { "url": "https://lyricwinter.com/mcp", "headers": { "Authorization": "Bearer lw_..." } } } } ``` ## Connect Codex Add the server to `~/.codex/config.toml`, then sign in: ```toml title="~/.codex/config.toml" [mcp_servers.lyricwinter] url = "https://lyricwinter.com/mcp" ``` ```bash codex mcp login lyricwinter ``` ## Connect other MCP clients Any client that supports remote MCP servers over Streamable HTTP can connect: 1. Point the client at `https://lyricwinter.com/mcp`. 2. If the client supports MCP authorization, it discovers LyricWinter's OAuth server from the `WWW-Authenticate` challenge and the protected-resource metadata at `https://lyricwinter.com/.well-known/oauth-protected-resource/mcp`, registers itself, and opens the LyricWinter sign-in page. 3. Otherwise, send a Studio-scoped API key as `Authorization: Bearer lw_...`. To test the server by hand, run the MCP Inspector and connect it to the same URL: ```bash npx @modelcontextprotocol/inspector ``` ## How does MCP authentication work? LyricWinter hosts its own OAuth 2.1 authorization server for the MCP server: 1. An unauthenticated request returns `401` with a `WWW-Authenticate` header that points to the protected-resource metadata. 2. The client registers through dynamic client registration (`/oauth/register`) and starts the authorization code flow with PKCE (`S256`). 3. You sign in to LyricWinter and approve `studio:read` and, if the client asks for them, `studio:write`, `voices:read`, and `voices:write`. 4. LyricWinter issues a one-hour access token and a 30-day refresh token that rotates on every use. Each token only reaches the account that approved it. To disconnect a client, revoke its key in the **Developers** panel of your [dashboard](/dashboard). See [Authentication](/docs/authentication#how-do-mcp-clients-authenticate) for endpoints and supported parameters. Clients that cannot complete OAuth can send a LyricWinter API key with the `studio:read` and `studio:write` scopes instead, plus `voices:read` and `voices:write` for the voice tools. ## What can I ask an agent to do? Once connected, ask in plain language. For example: - "Read this chapter aloud with a different voice for each character." - "Turn my fanfic into an audio drama." (paste the story) - "Give Mara a raspier, older voice." - "I want to narrate it in my own voice." - "Make a link I can share." - "Show my LyricWinter Studio projects." - "Create a Studio story called *The Lighthouse* from this text and prepare it for audio." - "Add this chapter to my *Winter Road* project." - "Generate audio for chapters 1 through 3 and tell me when it's done." - "Which characters speak in this project, and how often?" The agent picks the right [tools](/docs/mcp-tools) and polls workflows until they finish. The tool descriptions tell agents to confirm with you before starting paid audio generation. ## Does the MCP server cost anything? Connecting and reading are free, and new LyricWinter accounts include free words. `create_audio_story`, `recast_character`, and `start_audio_workflow` run Create Audio, which consumes words from your LyricWinter balance exactly as it does in the Studio app. These tools are marked as having side effects so that clients can ask for confirmation. ## What the MCP server does not do yet The beta MCP server covers the core story-to-audio loop. These tasks are available in the [REST API](/docs/api) and the [Studio app](/studio) but not yet as MCP tools: - Editing document blocks, or directing a specific line ("whisper this") - Exports to WAV, M4B, EPUB, or SRT - Uploading sounds or voice model files ## Troubleshooting | Symptom | Fix | | --- | --- | | The client says authorization is required or expired | Reconnect or re-authenticate the server in your client. Access tokens last one hour and refresh automatically when the client supports it. | | A tool returns "lacks this permission" | The token or API key is missing a scope the tool needs. Reconnect LyricWinter and approve access, or use a key with the listed scopes. | | `403 Origin is not allowed` | Browser-based clients must connect from `lyricwinter.com` or `chatgpt.com`. Use a desktop, CLI, or server-side client instead. | | A write tool timed out | The result includes a `mutation_id`. Call the tool again with the same `mutation_id` to retry safely without creating a duplicate or a second paid run. | --- Source: https://lyricwinter.com/docs/mcp-tools.md --- title: "LyricWinter MCP tools reference" description: "Every tool exposed by the LyricWinter Studio MCP server, with inputs, required scopes, side effects, and the recommended agent workflow." canonical_url: https://lyricwinter.com/docs/mcp-tools markdown_url: https://lyricwinter.com/docs/mcp-tools.md last_updated: 2026-09-30 status: beta --- # LyricWinter MCP tools reference > Every tool exposed by the LyricWinter Studio MCP server, with inputs, required scopes, side effects, and the recommended agent workflow. The LyricWinter MCP server exposes 22 tools. Read-only Studio tools need the `studio:read` scope; tools that create or change Studio content need `studio:write`. Voice tools need `voices:read` or `voices:write`. Every tool calls the [LyricWinter REST API](/docs/api) as the connected user. ## In ChatGPT, Claude, and other chat apps Chat apps that support interactive MCP Apps show a story player and a voice panel inside the conversation. The server tells the model to call `create_audio_story` once with the full story text: the player shows casting and generation progress, then plays the finished audio with the script. Use `play_story` to reopen a story, `share_story` for a public link, `find_voices` with `recast_character` to change a character's voice, and `add_my_voice` when the user wants their own voice. Listing tools and loading the widgets works before an account is linked; every tool that reads or changes an account asks the user to connect LyricWinter. ## Recommended workflow for coding agents For advanced work on existing Studio projects, use `list_projects` and `list_project_sections` to find IDs, read documents before changing them or generating audio, treat workflow starts as asynchronous and call `get_workflow` until they finish, and never repeat a paid audio start unless the user wants another run. A typical session: 1. `list_projects` to find the project, or `create_story` to start a new one. 2. `get_document` to read the script and its versions. 3. `start_audio_workflow` after the user confirms which sections to generate. 4. `get_workflow` until the run reaches `completed`, `partially_completed`, `failed`, or `cancelled`. 5. `get_section_media` or `get_playback_manifest` to return playable audio. ## Tool summary | Tool | Purpose | Scope | Side effects | | --- | --- | --- | --- | | `profile` | Identify the connected account | `studio:read` | None | | `create_audio_story` | Turn story text into fully cast audio with a player | `studio:write` | Creates content, consumes words | | `play_story` | Open the player for an existing story | `studio:write` | Prepares an MP3 mix | | `share_story` | Create a public listening link | `studio:write` | Publishes the story | | `find_voices` | Search the voice library or the user's voices | none, or `voices:read` | None | | `recast_character` | Change a character's voice and regenerate their lines | `studio:write`, `voices:read` | Changes casting, consumes words | | `add_my_voice` | Open the voice panel to add the user's own voice | `voices:write` | None until the user uploads | | `get_story_state` | Player only: refresh progress and the audio URL | `studio:write` | Prepares an MP3 mix | | `upload_voice_sample` | Voice panel only: create a private voice from a sample | `voices:write` | Creates a voice | | `get_voice_status` | Voice panel only: check voice processing | `voices:read` | None | | `list_projects` | List projects and standalone stories | `studio:read` | None | | `list_project_sections` | List a project's sections in order | `studio:read` | None | | `get_document` | Read a section's document | `studio:read` | None | | `list_project_speakers` | List a project's speakers | `studio:read` | None | | `create_story` | Create a standalone story | `studio:write` | Creates content | | `create_section` | Add a section to a project | `studio:write` | Creates content | | `prepare_sections` | Structure sections into a script | `studio:write` | Changes documents | | `start_audio_workflow` | Generate audio | `studio:write` | Consumes words | | `get_workflow` | Check workflow progress | `studio:read` | None | | `cancel_workflow` | Cancel a running workflow | `studio:write` | Stops work | | `get_section_media` | Get per-block audio for a section | `studio:read` | None | | `get_playback_manifest` | Get the compiled playback timeline | `studio:read` | None | ## Results and errors The chat-app tools (`create_audio_story`, `play_story`, `share_story`, `find_voices`, `recast_character`, `add_my_voice`) return a short summary for the model as `structuredContent`; the full script, line timings, and signed audio URL go only to the player in `_meta`. The other tools return the REST API's JSON response, including `data` and `request_id`. Tools that accept a `mutation_id` wrap it as `{ "result": ..., "mutation_id": "..." }` so the agent can retry the same change. A failed tool sets `isError` and returns the API error, plus the `mutation_id` when there is one. If LyricWinter could not confirm whether a write happened, the error says "No result was confirmed"; retry with the same `mutation_id`. ## Account ### `profile` Identifies the connected LyricWinter account. Returns `id`, `name`, and `email` as structured content. Takes no input. ## Projects and sections ### `list_projects` Lists the user's Studio projects and standalone stories with their IDs. Takes no input. Calls [`GET /studio/projects`](/docs/api/projects). ### `list_project_sections` Lists the sections of one project in order, including each section's ID and the project's `section_order_version`. Calls [`GET /studio/projects/{projectId}/sections`](/docs/api/sections). | Input | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | UUID | Yes | The project to list. | ### `create_story` Creates a standalone Studio story from text. Calls [`POST /studio/sections`](/docs/api/sections). | Input | Type | Required | Description | | --- | --- | --- | --- | | `title` | string, 1–500 characters | Yes | Story title. | | `raw_text` | string, 1–100,000 characters | Yes | The story text. | | `mutation_id` | UUID | No | Reuse only when retrying the exact same creation. Generated if omitted. | ### `create_section` Adds a section to an existing project. Read the project's sections first to get its current `section_order_version`. Calls [`POST /studio/projects/{projectId}/sections`](/docs/api/sections). | Input | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | UUID | Yes | The project to add to. | | `base_section_order_version` | integer or null | Yes | The `section_order_version` from `list_project_sections`. | | `title` | string, 1–500 characters | Yes | Section title. | | `raw_text` | string, 1–100,000 characters | Yes | The section text. | | `mutation_id` | UUID | No | Reuse only when retrying the exact same creation. | ## Documents and speakers ### `get_document` Reads a section's current document: its version, blocks, speakers, and edit targets. Agents should read the document before generating audio. Calls [`GET /studio/sections/{sectionId}/document`](/docs/api/documents). | Input | Type | Required | Description | | --- | --- | --- | --- | | `section_id` | UUID | Yes | The section to read. | ### `list_project_speakers` Lists the speakers used in a project and their casting status. Calls [`GET /studio/projects/{projectId}/speakers`](/docs/api/documents). | Input | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | UUID | Yes | The project to inspect. | ## Workflows ### `prepare_sections` Starts an asynchronous workflow that structures sections into narration and dialogue without generating audio. Poll `get_workflow` for completion. Calls [`POST /studio/projects/{projectId}/workflows`](/docs/api/workflows) with `workflow_kind: "prepare_sections"`. | Input | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | UUID | Yes | The project that owns the sections. | | `section_ids` | array of UUIDs, 1–100 | Yes | Sections to prepare. | | `mutation_id` | UUID | No | Reuse only when retrying the same request. | ### `start_audio_workflow` Starts the paid, asynchronous Create Audio workflow for selected sections. It prepares text, casts voices, directs performances, adds sound effects, and generates audio. Confirm the scope and cost with the user first, then poll `get_workflow`. Calls [`POST /studio/projects/{projectId}/workflows`](/docs/api/workflows) with `workflow_kind: "create_audio"`. | Input | Type | Required | Description | | --- | --- | --- | --- | | `project_id` | UUID | Yes | The project that owns the sections. | | `section_ids` | array of UUIDs, 1–12 | Yes | Sections to generate. | | `include_generated_sfx` | boolean | No | Add AI-generated sound effects. Defaults to `true`. | | `place_project_sounds` | boolean | No | Place sounds linked to the project. Defaults to `true`. | | `mutation_id` | UUID | No | Reuse only when retrying the same run. A new ID starts a new, separately billed run. | ### `get_workflow` Returns a workflow's status, current stage, step progress, warnings, and errors. Calls [`GET /studio/workflows/{workflowRunId}`](/docs/api/workflows). | Input | Type | Required | Description | | --- | --- | --- | --- | | `workflow_id` | UUID | Yes | The `run.id` returned when the workflow started. | ### `cancel_workflow` Requests cancellation of a running workflow. Agents should ask the user before cancelling. Calls [`POST /studio/workflows/{workflowRunId}/cancel`](/docs/api/workflows). | Input | Type | Required | Description | | --- | --- | --- | --- | | `workflow_id` | UUID | Yes | The workflow to cancel. | ## Media and playback ### `get_section_media` Returns the generated audio state of every block in a section, with short-lived signed playback URLs. Calls [`GET /studio/sections/{sectionId}/media`](/docs/api/media). | Input | Type | Required | Description | | --- | --- | --- | --- | | `section_id` | UUID | Yes | The section to inspect. | ### `get_playback_manifest` Returns the compiled playback timeline for a section. Pass the versions returned by `get_document`. Calls [`GET /studio/sections/{sectionId}/playback-manifest`](/docs/api/media). | Input | Type | Required | Description | | --- | --- | --- | --- | | `section_id` | UUID | Yes | The section to play. | | `document_version` | integer | Yes | `document.document_version` from `get_document`. | | `speaker_registry_version` | integer | Yes | `document.speaker_registry_version` from `get_document`. | ## Listening in chat apps ### `create_audio_story` Turns a story, fanfic, script, or chapter into fully cast audio. Creates a standalone story ([`POST /studio/sections`](/docs/api/sections)) and starts Create audio, which detects characters, casts voices, directs the performances, and adds sound. Returns the story player, which tracks progress on its own. Uses words from the user's balance. | Input | Type | Required | Description | | --- | --- | --- | --- | | `story_text` | string, 1–100,000 characters | Yes | The complete story text. | | `title` | string, 1–200 characters | No | Story title. Derived from the first line if omitted. | | `sound_effects` | boolean | No | Add ambient sound and sound effects. Defaults to `true`. | ### `play_story` Opens the player for a story the user already made. Prepares an MP3 mix of the current audio when needed. | Input | Type | Required | Description | | --- | --- | --- | --- | | `section_id` | UUID | No | The story to open. Defaults to the user's most recent story. | ### `share_story` Publishes a story and returns its public listening link. Anyone with the link can listen and read the script. Calls [`POST /studio/sections/{sectionId}/share`](/docs/api/sections). | Input | Type | Required | Description | | --- | --- | --- | --- | | `section_id` | UUID | Yes | The story to share. | ## Voices ### `find_voices` Searches LyricWinter's public voice library, or the user's own voices, and returns voice IDs for `recast_character`. Library search works without a linked account. | Input | Type | Required | Description | | --- | --- | --- | --- | | `query` | string, up to 200 characters | No | Words describing the voice, or a voice name. | | `mine` | boolean | No | Search only the user's voices. Requires a linked account. | ### `recast_character` Changes a character's voice with Studio's preferred engine for that voice, then regenerates the character's lines unless told not to. | Input | Type | Required | Description | | --- | --- | --- | --- | | `section_id` | UUID | Yes | The story. | | `character` | string | Yes | The character's name as shown in the cast. | | `voice_id` | UUID | Yes | A voice from `find_voices`. | | `regenerate_audio` | boolean | No | Regenerate the changed lines. Defaults to `true`. | ### `add_my_voice` Opens the voice panel, where the user records or uploads a 10–30 second sample of their own voice and confirms they have permission to use it. The new voice is private. | Input | Type | Required | Description | | --- | --- | --- | --- | | `voice_name` | string, 1–80 characters | No | Suggested name for the voice. | ## Player and voice panel These tools are called by the widgets and hidden from the model. ### `get_story_state` Refreshes a story's progress, cast, script timings, and signed audio URL. | Input | Type | Required | Description | | --- | --- | --- | --- | | `section_id` | UUID | Yes | The story. | | `workflow_id` | UUID or null | No | The Create audio run to track. | ### `upload_voice_sample` Creates a private voice from the user's sample through the same upload flow as LyricWinter's apps. | Input | Type | Required | Description | | --- | --- | --- | --- | | `name` | string, 1–80 characters | Yes | Voice name. | | `mime_type` | string | Yes | The sample's audio type, such as `audio/webm`. | | `data_base64` | string | Yes | The sample, base64-encoded, up to 2.5 MB. | | `permission_confirmed` | `true` | Yes | The user confirmed they have permission to use this voice. | ### `get_voice_status` Checks whether a new voice has finished processing. | Input | Type | Required | Description | | --- | --- | --- | --- | | `voice_id` | UUID | Yes | The voice to check. |