API reference
Voices and uploads API
Discover accessible voices, inspect current engine eligibility, and upload a private voice through the ordinary asynchronous upload flow. Retain original voice, resource and operation identifiers when recovering an interrupted upload.
BetaUpdated
- Base URL
- https://lyricwinter.com/api/v1
- Authentication
- Authorization: Bearer lw_…
- Contract
- openapi.json
Discover accessible voices
GET/voices
voices:readBrowse or search completed selectable voices in scope=mine, shared, or library. Library discovery works anonymously; private collections require voices:read. To receive pagination, send include_pagination=true with an explicit scope, without mine=true or sort=popular. Follow pagination.next_offset with the same scope, query and limit even when voices is empty: access or selectability filtering can remove rows after the page boundary. next_offset=null ends the result. Without this option, the existing voices-only payload is unchanged. context_project_id applies current Studio project authority and relevance. Results reflect current access and may change between reads; engine eligibility is checked separately.
Query parameters
qstringdefault ""Search text matched against voice name, description, tags, and the hybrid voice search RPC. Empty or omitted returns browse results.
limitinteger1–50default 20Maximum number of public/search results. Omitted means 20.
offsetinteger≥ 0default 0Non-negative integer offset. Omitted means 0.
minebooleandefault falseWhen true, returns only the authenticated caller's voices. Anonymous callers receive an empty list.
include_minebooleandefault trueWhen false, authenticated non-mine browse/search results omit the caller's owned voices. Only exact true/false strings are accepted.
scopestringLimits results to the caller's own voices, voices shared with the caller, or the public voice library.
One of
minesharedlibraryinclude_paginationbooleandefault falseOpt in to raw-page continuation. Requires explicit scope=mine, shared or library; cannot combine with mine=true or sort=popular. Follow pagination.next_offset even on an empty filtered page. Omitted or false preserves the legacy voices-only payload.
context_project_idstringuuidAuthenticated Studio project UUID. When present, owned AI-designed voices are limited to voices created for or actively used by this standalone or named project; omit it to request the full account catalog.
sortstringOnly the exact value popular changes behavior. Other values currently fall through to the normal list/search path.
Response 200
Processed voice picker results. Fields below are inside data.
voicesarray of objectrequired17 item attributes
idstringrequireduuidacoustic_revisioninteger≥ 1Stable logical voice acoustic revision. It increments when a reroll publishes new voice acoustics.
namestringrequireddescriptionstring | nullrequiredtagsarray of stringrequiredstatusstringrequiredavailable_modelsarray of stringrequireddefault_modelstring | nullrequiredis_selectablebooleanrequiredTrue when the voice is completed and has at least one available generation model.
is_user_createdbooleanrequiredTrue when the resolved authenticated user owns the voice.
is_publicbooleanrequiredis_shared_with_userbooleanrequiredTrue when an authenticated caller can access the voice through a share and does not own it.
voice_originstring | nullrequireddesign_providerstring | nullrequiredsample_resource_versionstring | nullrequiredCurrent sample resource version token, when a sample resource is available.
creator_usernamestring | nullrequiredCreator username for public or accepted-shared voices; null for private owned voices or profiles without a username.
creator_display_namestring | nullrequiredCreator profile display name for public or accepted-shared voices, used when no username exists.
paginationobject2 child attributes
next_offsetinteger | nullrequired≥ 0has_morebooleanrequired
Errors 400, 401, 500 use the standard error envelope.
curl "https://lyricwinter.com/api/v1/voices" \
-H "Authorization: Bearer $LYRICWINTER_API_KEY"const response = await fetch(`https://lyricwinter.com/api/v1/voices`, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.LYRICWINTER_API_KEY}`,
},
});
const { data, error, request_id } = await response.json();import os
import requests
response = requests.request(
"GET",
f"https://lyricwinter.com/api/v1/voices",
headers={
"Authorization": f"Bearer {os.environ['LYRICWINTER_API_KEY']}",
},
)
payload = response.json(){
"data": {
"voices": [
{
"id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10",
"acoustic_revision": 1,
"name": "My integration",
"description": "string",
"tags": [
"string"
],
"status": "string",
"available_models": [
"string"
],
"default_model": "string",
"is_selectable": true,
"is_user_created": true,
"is_public": true,
"is_shared_with_user": true,
"voice_origin": "string",
"design_provider": "string",
"sample_resource_version": "string",
"creator_username": "string",
"creator_display_name": "string"
}
],
"pagination": {
"next_offset": 1,
"has_more": true
}
},
"request_id": "req_01J9Z3K8QF4"
}Initiate a voice upload
POST/voices
voices:writeRetry with the same Idempotency-KeyCreate a private voice upload with permission to use the recording. Send exact file metadata and an Idempotency-Key; retain that key, metadata, voiceId and resource identifiers. After a lost response, send the identical key/body with Idempotency-Recovery: only; this never creates a replacement. Allocation is saved atomically before signing. Recovery returns current status and fresh targets only while uploading; later statuses return an empty signedUrls array. Upload original bytes with the returned Content-Type to the issued targets, then complete that same voice. Changed input conflicts. Missing/expired keys fail closed; older started keys remain in-progress. Valid older completed receipts recover their original IDs and current status. Key retention is 24 hours; a known voice ID remains readable with GET /voices/{voiceId}. Unknown after expiry never proves that creation did nothing. Initiation is not a completed clone. Requires current voices:write.
Headers
Idempotency-Keystring8–128 charspattern ^[\x21-\x7E]+$Opaque client-generated key for retrying one intended mutation. Accepted values are 8-128 visible ASCII characters. Reusing the same key with the same authenticated principal, method, route, scope, and normalized body replays the stored terminal response; reusing it for a different request returns a 409 conflict.
Idempotency-RecoverystringRecovery-only lookup: requires the original Idempotency-Key and identical metadata. Never allocates a voice or resources; unavailable after the original 24-hour retention.
One of
only
Request body application/json
namestringrequired1–80 charsdescriptionstringrequired1–255 charstagsstring | array of stringrequiredString inputs are comma-split by the service. Parsed tags must contain 1-20 entries, each 100 characters or fewer.
isPublicbooleanrequiredCurrently must be false; public voice publishing is disabled during initiation.
uploadTypestringrequiredmin 1 charsfilesarray of objectrequiredmin 1 items4 item attributes
filenamestringrequiredmin 1 charssizenumberrequired≥ 0Client-reported file size in bytes. AUDIO_SAMPLE_* files are rejected above 25 MiB by the service.
contentTypestringContent type used when signing the GCS write URL.
file_typestringrequiredmin 1 charsCurrent callers use values such as AUDIO_SAMPLE_WAV, AUDIO_SAMPLE_MP3, RVC_SAFETENSORS, and RVC_INDEX.
Response 201
Voice upload records and signed direct-upload URLs were created. Fields below are inside data.
successtruerequiredvoiceIdstringrequireduuidsignedUrlsarray of objectrequired4 item attributes
resourceIdstringrequireduuidsignedUrlstringrequireduriShort-lived v4 signed GCS write URL. Current implementation signs for 15 minutes.
gcsPathstringrequiredQuarantine bucket object path created for this resource.
contentTypestringEchoed content type when one was provided for signing.
messagestringrequiredstatusstringPresent on recovery-only responses and advanced replays. Upload targets are empty unless status is uploading; acceptance is not processing completion.
One of
uploadingpendingprocessingcompletedfailed
Errors 400, 401, 403, 409, 500, 503 use the standard error envelope.
curl -X POST "https://lyricwinter.com/api/v1/voices" \
-H "Authorization: Bearer $LYRICWINTER_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"name": "My integration",
"description": "string",
"tags": "string",
"isPublic": true,
"uploadType": "string",
"files": [
{
"filename": "string",
"size": 0,
"file_type": "string"
}
]
}'const response = await fetch(`https://lyricwinter.com/api/v1/voices`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LYRICWINTER_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"name": "My integration",
"description": "string",
"tags": "string",
"isPublic": true,
"uploadType": "string",
"files": [
{
"filename": "string",
"size": 0,
"file_type": "string"
}
]
}),
});
const { data, error, request_id } = await response.json();import os, uuid
import requests
response = requests.request(
"POST",
f"https://lyricwinter.com/api/v1/voices",
headers={
"Authorization": f"Bearer {os.environ['LYRICWINTER_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"name": "My integration",
"description": "string",
"tags": "string",
"isPublic": True,
"uploadType": "string",
"files": [
{
"filename": "string",
"size": 0,
"file_type": "string"
}
]
},
)
payload = response.json(){
"data": {
"success": true,
"voiceId": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10",
"signedUrls": [
{
"resourceId": "c9f0f895-fb98-4b91-9e3f-1d2a6b7c8d20",
"signedUrl": "https://lyricwinter.com/…",
"gcsPath": "string",
"contentType": "string"
}
],
"message": "string",
"status": "uploading"
},
"request_id": "req_01J9Z3K8QF4"
}Read accessible voices by exact IDs
POST/voices/by-ids
voices:readHydrate exact voice identifiers through current owned, shared and public access checks. Missing or inaccessible voices are not evidence of a different replacement voice. Use the returned IDs and current status rather than matching display names.
Request body application/json
idsarray of stringrequired1–100 items
Response 200
Processed voice picker results. Fields below are inside data.
voicesarray of objectrequired17 item attributes
idstringrequireduuidacoustic_revisioninteger≥ 1Stable logical voice acoustic revision. It increments when a reroll publishes new voice acoustics.
namestringrequireddescriptionstring | nullrequiredtagsarray of stringrequiredstatusstringrequiredavailable_modelsarray of stringrequireddefault_modelstring | nullrequiredis_selectablebooleanrequiredTrue when the voice is completed and has at least one available generation model.
is_user_createdbooleanrequiredTrue when the resolved authenticated user owns the voice.
is_publicbooleanrequiredis_shared_with_userbooleanrequiredTrue when an authenticated caller can access the voice through a share and does not own it.
voice_originstring | nullrequireddesign_providerstring | nullrequiredsample_resource_versionstring | nullrequiredCurrent sample resource version token, when a sample resource is available.
creator_usernamestring | nullrequiredCreator username for public or accepted-shared voices; null for private owned voices or profiles without a username.
creator_display_namestring | nullrequiredCreator profile display name for public or accepted-shared voices, used when no username exists.
paginationobject2 child attributes
next_offsetinteger | nullrequired≥ 0has_morebooleanrequired
Errors 400, 401, 500 use the standard error envelope.
curl -X POST "https://lyricwinter.com/api/v1/voices/by-ids" \
-H "Authorization: Bearer $LYRICWINTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"ids": [
"8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10"
]
}'const response = await fetch(`https://lyricwinter.com/api/v1/voices/by-ids`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LYRICWINTER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"ids": [
"8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10"
]
}),
});
const { data, error, request_id } = await response.json();import os
import requests
response = requests.request(
"POST",
f"https://lyricwinter.com/api/v1/voices/by-ids",
headers={
"Authorization": f"Bearer {os.environ['LYRICWINTER_API_KEY']}",
},
json={
"ids": [
"8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10"
]
},
)
payload = response.json(){
"data": {
"voices": [
{
"id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10",
"acoustic_revision": 1,
"name": "My integration",
"description": "string",
"tags": [
"string"
],
"status": "string",
"available_models": [
"string"
],
"default_model": "string",
"is_selectable": true,
"is_user_created": true,
"is_public": true,
"is_shared_with_user": true,
"voice_origin": "string",
"design_provider": "string",
"sample_resource_version": "string",
"creator_username": "string",
"creator_display_name": "string"
}
],
"pagination": {
"next_offset": 1,
"has_more": true
}
},
"request_id": "req_01J9Z3K8QF4"
}Read voice processing status
GET/voices/{voiceId}
voices:readRead the accessible original voice's current status and acoustic revision. Upload completion can return while processing is pending; poll this read rather than initiating another upload. Completed processing and current engine eligibility are separate checks.
Path parameters
voiceIdstringrequireduuid
Response 200
Managed voice details and caller admin status. Fields below are inside data.
voiceobjectrequired23 child attributes
idstringrequireduuidacoustic_revisionintegerrequired≥ 1namestringrequireddescriptionstring | nullrequiredtagsarray of stringrequiredis_publicbooleanrequiredcreated_atstringrequireddate-timeupdated_atstringrequireddate-timestatusstringrequirederror_messagestring | nullvoice_originstring | nulldesign_providerstring | nullpublic_visibility_lockedboolean | nullpublic_visibility_locked_atstring | nulldate-timepublic_visibility_lock_reasonstring | nullis_shared_with_userbooleancreator_usernamestring | nullrequiredcreator_display_namestring | nullrequiredcan_update_metadatabooleanrequiredcan_deletebooleanrequiredcan_rerollbooleanrequiredcan_replace_samplefalserequiredReserved for a future same-ID sample replacement contract; v1 does not support that operation.
resourcesarray of object | null8 item attributes
idstring | nulluuidacoustic_revisioninteger | null≥ 1resource_typestring | nullgcs_pathstring | nullmetadataobject | nullcreated_atstring | nulldate-timestatusstring | nullerror_messagestring | null
isAdminbooleanrequired
Errors 400, 401, 403, 404, 500 use the standard error envelope.
curl "https://lyricwinter.com/api/v1/voices/$VOICE_ID" \
-H "Authorization: Bearer $LYRICWINTER_API_KEY"const voiceId = "<voiceId>";
const response = await fetch(`https://lyricwinter.com/api/v1/voices/${voiceId}`, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.LYRICWINTER_API_KEY}`,
},
});
const { data, error, request_id } = await response.json();import os
import requests
voice_id = "<voiceId>"
response = requests.request(
"GET",
f"https://lyricwinter.com/api/v1/voices/{voice_id}",
headers={
"Authorization": f"Bearer {os.environ['LYRICWINTER_API_KEY']}",
},
)
payload = response.json(){
"data": {
"voice": {
"id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10",
"acoustic_revision": 1,
"name": "My integration",
"description": "string",
"tags": [
"string"
],
"is_public": true,
"created_at": "2026-09-30T17:00:00.000Z",
"updated_at": "2026-09-30T17:00:00.000Z",
"status": "string",
"error_message": "string",
"voice_origin": "string",
"design_provider": "string",
"public_visibility_locked": true,
"public_visibility_locked_at": "2026-09-30T17:00:00.000Z",
"public_visibility_lock_reason": "string",
"is_shared_with_user": true,
"creator_username": "string",
"creator_display_name": "string",
"can_update_metadata": true,
"can_delete": true,
"can_reroll": true,
"can_replace_sample": false,
"resources": [
{
"id": "c9f0f895-fb98-4b91-9e3f-1d2a6b7c8d20",
"acoustic_revision": 1,
"resource_type": "string",
"gcs_path": "string",
"metadata": {},
"created_at": "2026-09-30T17:00:00.000Z",
"status": "string",
"error_message": "string"
}
]
},
"isAdmin": true
},
"request_id": "req_01J9Z3K8QF4"
}Read current voice engine options
GET/voices/{voiceId}/engine-samples
voices:readRead current-account engine availability, preparation requirements, reasons, acoustic revision and existing samples for this exact voice. This read neither selects an engine nor generates a sample. Check the returned status and profile before casting or requesting generation; do not silently substitute another engine.
Path parameters
voiceIdstringrequireduuid
Response 200
Current voice engine sample catalog. Fields below are inside data.
schema_version1requiredvoice_idstringrequireduuidacoustic_revisionintegerrequired≥ 1canonical_textobjectrequired2 child attributes
version"voice-engine-sample-en-v1"requiredtext"At first light, Rowan whispered, "We made it," then laughed softly into the rain."required
default_modelstring | nullrequiredmodel_optionsarray of objectrequiredmax 50 items8 item attributes
providerstringrequiredmodelstringrequiredlabelstringrequiredavailabilitystringrequiredOne of
readypreparation_requiredunavailablereasonstring | nullrequiredcompiler_versionstringrequiredestimated_snowcoinsintegerrequired≥ 0sampleobject | nullrequired18 child attributes
schema_version1requiredsample_idstringrequireduuidvoice_idstringrequireduuidacoustic_revisionintegerrequired≥ 1providerstringrequired1–100 charsmodelstringrequired1–100 charscanonical_text_versionstringrequiredOne of
voice-engine-sample-en-v1voice-preview-line-v1compiler_versionstringrequired1–200 charsstatusstringrequiredOne of
preparing_voicequeuedrunningcompletedfailedexpiredcache_statusstring | nullrequiredOne of
misscoalescedhitestimated_snowcoinsintegerrequired≥ 0charged_snowcoinsintegerrequired≥ 0audio_urlstring | nullrequireduriaudio_url_expires_atstring | nullrequireddate-timeerrorobject | nullrequired3 child attributes
codestringrequired1–120 charsmessagestringrequired1–500 charsretryablebooleanrequired
created_atstringrequireddate-timeupdated_atstringrequireddate-timeexpires_atstringrequireddate-time
Errors 400, 401, 403, 404, 429, 500 use the standard error envelope.
curl "https://lyricwinter.com/api/v1/voices/$VOICE_ID/engine-samples" \
-H "Authorization: Bearer $LYRICWINTER_API_KEY"const voiceId = "<voiceId>";
const response = await fetch(`https://lyricwinter.com/api/v1/voices/${voiceId}/engine-samples`, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.LYRICWINTER_API_KEY}`,
},
});
const { data, error, request_id } = await response.json();import os
import requests
voice_id = "<voiceId>"
response = requests.request(
"GET",
f"https://lyricwinter.com/api/v1/voices/{voice_id}/engine-samples",
headers={
"Authorization": f"Bearer {os.environ['LYRICWINTER_API_KEY']}",
},
)
payload = response.json(){
"data": {
"schema_version": 1,
"voice_id": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10",
"acoustic_revision": 1,
"canonical_text": {
"version": "voice-engine-sample-en-v1",
"text": "At first light, Rowan whispered, \"We made it,\" then laughed softly into the rain."
},
"default_model": "string",
"model_options": [
{
"provider": "string",
"model": "string",
"label": "string",
"availability": "ready",
"reason": "string",
"compiler_version": "string",
"estimated_snowcoins": 1,
"sample": {
"schema_version": 1,
"sample_id": "c9f0f895-fb98-4b91-9e3f-1d2a6b7c8d20",
"voice_id": "45c48cce-2e2d-4fbd-8a1c-5e6f7a8b9c30",
"acoustic_revision": 1,
"provider": "string",
"model": "string",
"canonical_text_version": "voice-engine-sample-en-v1",
"compiler_version": "string",
"status": "preparing_voice",
"cache_status": "miss",
"estimated_snowcoins": 1,
"charged_snowcoins": 1,
"audio_url": "https://lyricwinter.com/…",
"audio_url_expires_at": "2026-09-30T17:00:00.000Z",
"error": {
"code": "string",
"message": "string",
"retryable": true
},
"created_at": "2026-09-30T17:00:00.000Z",
"updated_at": "2026-09-30T17:00:00.000Z",
"expires_at": "2026-09-30T17:00:00.000Z"
}
}
]
},
"request_id": "req_01J9Z3K8QF4"
}Refresh original upload targets
POST/voices/{voiceId}/uploads/refresh
voices:writeRenew expiring upload targets for the same uploading voice and original resources. This does not create a replacement voice. Keep the original identifiers and upload only to the newly returned targets. Requires voices:write and authority over the upload.
Path parameters
voiceIdstringrequireduuid
Response 200
Voice upload records and signed direct-upload URLs were created. Fields below are inside data.
successtruerequiredvoiceIdstringrequireduuidsignedUrlsarray of objectrequired4 item attributes
resourceIdstringrequireduuidsignedUrlstringrequireduriShort-lived v4 signed GCS write URL. Current implementation signs for 15 minutes.
gcsPathstringrequiredQuarantine bucket object path created for this resource.
contentTypestringEchoed content type when one was provided for signing.
messagestringrequiredstatusstringPresent on recovery-only responses and advanced replays. Upload targets are empty unless status is uploading; acceptance is not processing completion.
One of
uploadingpendingprocessingcompletedfailed
Errors 400, 401, 404, 409, 503 use the standard error envelope.
curl -X POST "https://lyricwinter.com/api/v1/voices/$VOICE_ID/uploads/refresh" \
-H "Authorization: Bearer $LYRICWINTER_API_KEY"const voiceId = "<voiceId>";
const response = await fetch(`https://lyricwinter.com/api/v1/voices/${voiceId}/uploads/refresh`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LYRICWINTER_API_KEY}`,
},
});
const { data, error, request_id } = await response.json();import os
import requests
voice_id = "<voiceId>"
response = requests.request(
"POST",
f"https://lyricwinter.com/api/v1/voices/{voice_id}/uploads/refresh",
headers={
"Authorization": f"Bearer {os.environ['LYRICWINTER_API_KEY']}",
},
)
payload = response.json(){
"data": {
"success": true,
"voiceId": "8f14e45f-ceea-4e7a-9c2d-3b1a5f2e7c10",
"signedUrls": [
{
"resourceId": "c9f0f895-fb98-4b91-9e3f-1d2a6b7c8d20",
"signedUrl": "https://lyricwinter.com/…",
"gcsPath": "string",
"contentType": "string"
}
],
"message": "string",
"status": "uploading"
},
"request_id": "req_01J9Z3K8QF4"
}Complete the original voice upload
POST/voices/{voiceId}/uploads/complete
voices:writeRetry with the same Idempotency-KeyFinalize the existing voice after its files have been uploaded. Use an Idempotency-Key and retain it together with the original voice ID on uncertainty; retry only that same completion. Acceptance starts or recovers asynchronous processing and does not mean the clone is ready. Read the voice's current status and engine options before use. Requires voices:write.
Path parameters
voiceIdstringrequireduuid
Headers
Idempotency-Keystring8–128 charspattern ^[\x21-\x7E]+$Opaque client-generated key for retrying one intended mutation. Accepted values are 8-128 visible ASCII characters. Reusing the same key with the same authenticated principal, method, route, scope, and normalized body replays the stored terminal response; reusing it for a different request returns a 409 conflict.
Response 202
Voice upload finalization was accepted and background processing was scheduled. Fields below are inside data.
successtruerequiredmessagestringrequired
Errors 400, 401, 403, 404, 409, 500, 503 use the standard error envelope.
curl -X POST "https://lyricwinter.com/api/v1/voices/$VOICE_ID/uploads/complete" \
-H "Authorization: Bearer $LYRICWINTER_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"const voiceId = "<voiceId>";
const response = await fetch(`https://lyricwinter.com/api/v1/voices/${voiceId}/uploads/complete`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LYRICWINTER_API_KEY}`,
"Idempotency-Key": crypto.randomUUID(),
},
});
const { data, error, request_id } = await response.json();import os, uuid
import requests
voice_id = "<voiceId>"
response = requests.request(
"POST",
f"https://lyricwinter.com/api/v1/voices/{voice_id}/uploads/complete",
headers={
"Authorization": f"Bearer {os.environ['LYRICWINTER_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
)
payload = response.json(){
"data": {
"success": true,
"message": "string"
},
"request_id": "req_01J9Z3K8QF4"
}