Get started
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.
BetaUpdated
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.
curland a terminal. The last section also shows the same flow in JavaScript and Python.
Step 1: Create an API key#
- Open your LyricWinter dashboard and expand Developers.
- Create a key with the
studio:read,studio:write, andaccount:readscopes. - Copy the key. It starts with
lw_and is shown only once.
Export it so the examples below can use it:
export LYRICWINTER_API_KEY="lw_..."Check that the key works:
curl https://lyricwinter.com/api/v1/me \
-H "Authorization: Bearer $LYRICWINTER_API_KEY"Every successful response is wrapped in { "data": ..., "request_id": "..." }. See 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_ididentifies your client session. Generate one per process and reuse it.client_mutation_ididentifies this one change. Reuse it only to retry the exact same request.
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:
{
"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"
}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.
export PROJECT_ID="<data.project_id>"
export SECTION_ID="<data.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.
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. |
export WORKFLOW_ID="<data.run.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:
curl "https://lyricwinter.com/api/v1/studio/sections/$SECTION_ID/media" \
-H "Authorization: Bearer $LYRICWINTER_API_KEY"{
"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.
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:
GET /studio/sections/{sectionId}/documentreturnsdata.document.document_versionanddata.document.speaker_registry_version.GET /studio/projects/{projectId}/mastering-profilereturnsdata.mastering_version.
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:
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));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.
- Handle conflicts, retries, and errors with Errors and retries.
- Browse every endpoint in the API reference.
- Let an AI agent do all of this for you with the MCP server.