Skip to content

Guides

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.

BetaUpdated

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 kindWhat it is
narrationNarrated prose, voiced by the narrator.
speechA line of dialogue attributed to a speaker.
sfxA sound effect placed in the timeline.
noteA note kept in the script but not voiced.
excludedSource text kept in the script but intentionally left out of the audio.
unparsedText 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.

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.

ModelProviderAccepted options
eleven_v4elevenlabsstability 0-1; integer seed 1-4294967295. Empty {} preserves defaults.
inworld-2inworldapply_text_normalization: APPLY_TEXT_NORMALIZATION_UNSPECIFIED, ON, or OFF.
inworld-1.5-max, inworld-1.5-miniinworldThe normalization option above; speaking_rate 0.5-1.5; temperature greater than 0 and at most 2.
cartesia-sonic-3.6, cartesia-sonic-3.5cartesiaspeed 0.6-1.5; volume 0.5-2.
fishaudio, fishaudio-s2.1-pro, fishaudio-drama-3fish_audiotemperature and top_p 0-1; latency: low, normal, or balanced; integer chunk_length 100-300; boolean normalize and condition_on_previous_chunks.
lightning_v3.1smallest_ai{} only.
gpt-4o-mini-tts, tts-1-hd, tts-1openai{} only.
f5ttsf5tts{} only.
stable-audio-3-medium (generated SFX only)stable_audioInteger 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.

Fish Audio Drama 3 is an explicit engine choice: use provider: "fish_audio" and model: "fishaudio-drama-3". It reuses the same Fish voice reference as the other Fish engines and uses 2× words for generated speech and engine samples. A completed sample for the voice's current revision can prepare that reference automatically during Create Audio. Manual Fish voice preparation continues to use model: "fishaudio-s2.1-pro"; it does not change the speaker's selected synthesis model.

Drama 3 accepts open-vocabulary [directions] before controlled speech, inline sounds and pauses, and paired <whisper>, <emphasis>, <soft>, <fast>, <slow>, <stress>, and <forceful> tags around specific words. Existing Fish phoneme pronunciation controls also apply. Adjacent directions are retained; Studio omits malformed cues and terminal directions with no following speech. Saved source text remains unchanged.

Preparing a speaker for ElevenLabs v4#

ElevenLabs v4 requires a paid plan. Free accounts can use other engines, and existing audio remains playable. Requests to select v4 or create new v4 audio without an active paid plan return 403 VOICE_MODEL_SUBSCRIPTION_REQUIRED.

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", expected_source_voice_id set to the speaker's saved voice_id, 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 any new provider_asset preparation, expected_source_voice_id pins the intended assigned voice. First admission rejects a different current cast rather than preparing the replacement voice. Once accepted, an identical request returns the original job even if casting later changes. Save the exact request and client_mutation_id; if acceptance is uncertain, retry both unchanged instead of adding the assertion to an existing request or generating a new mutation ID. Legacy requests without the assertion remain supported. This field does not apply to AI voice design.

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:

VersionProtectsWhere to read it
document_versionOne section's documentGET /studio/sections/{sectionId}/document
speaker_registry_versionThe project's speakers and castingThe same document response
section_order_versionThe order of sections in a projectGET /studio/projects/{projectId}/sections
mastering_versionLoudness and mastering settingsGET /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.

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_kindWhat it doesCostSections per run
prepare_sectionsStructures text into narration and dialogue blocks. Use it to review the script before generating audio.No audio is generated.Up to 100
create_audioRuns 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 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 plan access, the effective model, voice preparation, and compiler constraints. ElevenLabs v4 is unavailable on free plans, including for already prepared voices. Paid plans include a monthly allowance for custom voice clones with ElevenLabs: MyWinter 20, UltraWinter 60. New or renewed clones count; reusing a current clone and other engines do not. The allowance resets on the first of each month at 00:00 UTC, including annual plans. If cloning is unavailable, paid accounts can still use prepared current-revision voices. 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.