---
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="<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`.

> [!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="<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`:

```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).
