Voices
List, create, design, read, change, copy and delete saved voices, and fetch their clips.
Access: Keys with the media:read or media:generate scope, on an account with media access granted by Albastar Corp.
/v1/media/voices holds the saved voices of an account, vo_…, the shared voices of Albastar Corp., and lists the
built-in voices of each model. GET routes need the media:read scope; the other routes need media:generate. The
examples use Narrator A, the shared voice vo_tBeDCRb2byGFjB6HGyQq6a: a synthetic voice made with the Kokoro
built-in voice af_heart.
Errors on these routes have the shape {"ok": false, "error": "voice-not-found", "code": "voice_not_found", "message": "...", "param": "voice"}. Branch on code; show message to a person. Key, scope and rate-limit errors:
Errors.
The voice object
| Field | Type | Value |
|---|---|---|
id | string | vo_ and 22 random characters; a built-in voice keeps its name, such as af_heart |
object | string | voice |
name | string | 1 to 100 characters |
description | string | 0 to 500 characters; the description of a designed voice |
category | string | cloned (from a clip), designed (from a description) or builtin (a model's own voice) |
visibility | string | private for a voice of the key's account; public or members for a shared voice, which every key may use |
owner | string | you for a voice of the key's account, albastar for a shared or built-in voice |
status | string | ready; pending while a designed voice is made; failed |
error | string or null | Why a designed voice failed |
language | string | A language code such as en or en-gb, set by the voice's account |
gender, accent | string | Free text set by the voice's account |
labels | object | Up to 10 string keys and string values, such as {"use": "narration"} |
duration_s | number or null | The clip's length; null for a WebM clip, a built-in voice and a designed voice still pending |
transcript | string | The words of the clip; filled on first use when empty |
preview_url | string or null | A short line spoken in the voice; null until made |
preview_job | string or null | The job making the preview or the designed voice |
audio_url | string or null | The reference clip; null for a built-in voice and a voice that is not ready |
default_model | string or null | For a saved voice, the model a speech request without model runs: omnivoice, or the first of models while omnivoice is not in the model list; null when no model speaks saved voices. For a built-in voice, the voice's model |
models | array | Every model that speaks the voice, by model id |
usage_count | integer | Jobs submitted with the voice, the preview excluded |
created_at, updated_at | string or null | RFC 3339 times; null for a built-in voice |
consent | string | The consent note; present on the account's own voices only |
List voices
GET /v1/media/voices returns a page of voices the key may use, newest first.
Parameters
| Name | Type | Description |
|---|---|---|
scope | string | all (default): the account's voices and every shared voice; mine; shared |
model | string | Only voices the model speaks. The model's built-in voices come first on the first page and do not count toward limit; a model without saved voices, such as kokoro, lists its built-in voices only |
category | string | cloned, designed or builtin; builtin needs model |
language | string | en matches en and every en-… |
q | string | Text in the name or description, any case; for a built-in voice, text in the id |
limit | integer | 1 to 100, default 20 |
cursor | string | next_cursor of the page before |
Request
curl
curl -G https://api.albastov.com/v1/media/voices \
-H "Authorization: Bearer $ALBASTAR_API_KEY" \
-d model=kokoro -d language=en-gb -d limit=5Python
import json
import os
import urllib.parse
import urllib.request
query = urllib.parse.urlencode(
{"model": "kokoro", "language": "en-gb", "limit": 5}
)
req = urllib.request.Request(
f"https://api.albastov.com/v1/media/voices?{query}",
headers={
"User-Agent": "my-app/1.0",
"Authorization": f"Bearer {os.environ['ALBASTAR_API_KEY']}",
},
)
with urllib.request.urlopen(req, timeout=30) as res:
for v in json.load(res)["data"]:
print(v["id"], v["gender"])JavaScript
const query = new URLSearchParams({
model: "kokoro",
language: "en-gb",
limit: "5",
});
const res = await fetch(`https://api.albastov.com/v1/media/voices?${query}`, {
headers: { Authorization: `Bearer ${process.env.ALBASTAR_API_KEY}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
for (const v of (await res.json()).data) console.log(v.id, v.gender);Response
{
"object": "list",
"data": [
{
"id": "bf_alice",
"object": "voice",
"name": "bf_alice",
"description": "",
"category": "builtin",
"visibility": "public",
"owner": "albastar",
"status": "ready",
"error": null,
"language": "en-gb",
"gender": "female",
"accent": "",
"labels": {},
"duration_s": null,
"transcript": "",
"preview_url": null,
"preview_job": null,
"audio_url": null,
"default_model": "kokoro",
"models": ["kokoro"],
"usage_count": 0,
"created_at": null,
"updated_at": null
}
],
"has_more": false,
"next_cursor": null
}The response above is shortened to one of eight voices. kokoro speaks no saved voices, so the list holds its eight
built-in en-gb voices: bf_alice, bf_emma, bf_isabella, bf_lily, bm_daniel, bm_fable, bm_george and
bm_lewis. Built-in voices do not count toward limit, so limit=5 returns all eight.
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | bad-param | scope, category, model, limit or cursor is not valid; param names the field |
Create a voice
POST /v1/media/voices saves a clip of 3 to 60 s as a cloned voice. The voice is ready at once; the preview
follows.
Parameters
A multipart/form-data body with file, or a JSON body with from. An unknown field fails with 400 bad-param.
| Name | Type | Description |
|---|---|---|
file required in multipart | file | WAV, MP3, M4A, Ogg, WebM or FLAC, 3 to 60 s, 20 MB at most. The length is read from the file's header; a file without one is refused, except WebM |
from required in JSON | string | An upload of the account up_…, or a result of the account's job job:<id>/<i> |
name | string | 1 to 100 characters. Required in JSON; in multipart, defaults to the file name without its extension |
description | string | 500 characters at most |
language | string | A language code such as en, 35 characters at most |
gender | string | 40 characters at most |
accent | string | 60 characters at most |
labels | object | Up to 10 keys of 1 to 40 characters, string values of 100 at most; in multipart, the object as JSON text |
transcript | string | The words of the clip, 5,000 characters at most |
consent | string | Who the speaker is and what the speaker agreed to, 2,000 characters at most |
curl reads ; in a -F value as a field separator: put a value with ; in quotes inside the value
(-F 'consent="a; b"').
Request
The example downloads the reference clip of Narrator A and saves the clip as a voice of the account.
curl
curl -o reference.mp3 \
https://api.albastov.com/v1/media/samples/reference-848c3d555461736a.mp3
curl https://api.albastov.com/v1/media/voices \
-H "Authorization: Bearer $ALBASTAR_API_KEY" \
-F [email protected] \
-F name="Narrator A" \
-F language=en \
-F 'labels={"use": "narration"}' \
-F consent="Synthetic voice made with the Kokoro voice af_heart. No speaker."Python
import json
import os
import httpx
BASE = "https://api.albastov.com/v1"
AUTH = {"Authorization": f"Bearer {os.environ['ALBASTAR_API_KEY']}"}
clip = httpx.get(f"{BASE}/media/samples/reference-848c3d555461736a.mp3")
clip.raise_for_status()
res = httpx.post(
f"{BASE}/media/voices",
headers=AUTH,
files={"file": ("reference.mp3", clip.content, "audio/mpeg")},
data={
"name": "Narrator A",
"language": "en",
"labels": json.dumps({"use": "narration"}),
"consent": "Synthetic voice made with the Kokoro voice af_heart. "
"No speaker.",
},
timeout=60,
)
res.raise_for_status()
print(res.json()["id"])JavaScript
const base = "https://api.albastov.com/v1";
const clip = await fetch(
`${base}/media/samples/reference-848c3d555461736a.mp3`,
);
if (!clip.ok) throw new Error(`${clip.status} ${await clip.text()}`);
const form = new FormData();
form.append("file", await clip.blob(), "reference.mp3");
form.append("name", "Narrator A");
form.append("language", "en");
form.append("labels", JSON.stringify({ use: "narration" }));
form.append(
"consent",
"Synthetic voice made with the Kokoro voice af_heart. No speaker.",
);
const res = await fetch(`${base}/media/voices`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.ALBASTAR_API_KEY}` },
body: form,
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log((await res.json()).id);A clip already on the account needs no second upload: a JSON body with from set to an upload up_… or a job
result job:<id>/<i> takes the place of file. In JSON, name is required.
curl https://api.albastov.com/v1/media/voices \
-H "Authorization: Bearer $ALBASTAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"from": "up_Q8mZr3kPwD5nTb7yXc2hLf", "name": "Narrator A",
"consent":
"Synthetic voice made with the Kokoro voice af_heart. No speaker."}'Response
201 with the voice object.
{
"id": "vo_FLUWnoRjtFXyrBE8uWoTxL",
"object": "voice",
"name": "Narrator A",
"description": "",
"category": "cloned",
"visibility": "private",
"owner": "you",
"status": "ready",
"error": null,
"language": "en",
"gender": "",
"accent": "",
"labels": {"use": "narration"},
"duration_s": 14.04,
"transcript": "",
"preview_url": null,
"preview_job": "mj_uyeqTjDrzLxNLPpBKuibEi",
"audio_url": "…/v1/media/voices/vo_FLUWnoRjtFXyrBE8uWoTxL/audio",
"default_model": "omnivoice",
"models": [
"breeze2", "chatterbox", "cosyvoice3", "fireredtts3", "fish-s2pro",
"higgs3", "indextts2.5", "miotts", "moss-local", "omnivoice",
"qwen3-tts", "voxcpm2"
],
"usage_count": 0,
"created_at": "2026-11-03T04:03:16.434Z",
"updated_at": "2026-11-03T04:03:16.434Z",
"consent": "Synthetic voice made with the Kokoro voice af_heart. No speaker."
}The response above shortens audio_url to its path; the URL starts with https://api.albastov.com.
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | bad-param | A field is missing, unknown or too long, the clip is outside 3 to 60 s, or its header has no length; param names the field (file, from, name, labels, ...) |
| 400 | bad-upload | The multipart body has no boundary or did not arrive whole |
| 400 | bad-json | The JSON body is not an object |
| 413 | too-large | The clip is over 20 MB |
| 415 | not-audio | The clip is not WAV, MP3, M4A, Ogg, WebM or FLAC |
| 415 | multipart-or-json | The body is neither multipart nor JSON |
| 422 | voice_quota | The account already has quota voices, 30 by default |
Design a voice
POST /v1/media/voices/design makes a synthetic voice from a description. The answer is 202 with status
pending; GET /v1/media/voices/{id} returns ready when the voice is made, or failed with error. The clip the
model makes becomes the voice's reference clip and preview.
Parameters
A JSON body. A field outside the list fails with 400 bad-param.
| Name | Type | Description |
|---|---|---|
name required | string | 1 to 100 characters |
description required | string | The voice to make, such as "female, warm, low pitch, slow", 500 characters at most |
model | string | omnivoice (default) |
text | string | The line the voice speaks in its clip, 1,000 characters at most; default a fixed passage of about 15 s |
language | string | A language code for text |
Request
curl
curl https://api.albastov.com/v1/media/voices/design \
-H "Authorization: Bearer $ALBASTAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Night radio",
"description": "male, middle-aged, low pitch, slow and warm"}'Python
import json
import os
import urllib.request
body = {
"name": "Night radio",
"description": "male, middle-aged, low pitch, slow and warm",
}
req = urllib.request.Request(
"https://api.albastov.com/v1/media/voices/design",
data=json.dumps(body).encode(),
headers={
"User-Agent": "my-app/1.0",
"Authorization": f"Bearer {os.environ['ALBASTAR_API_KEY']}",
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(req, timeout=60) as res:
voice = json.load(res)
print(voice["id"], voice["status"])JavaScript
const res = await fetch("https://api.albastov.com/v1/media/voices/design", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ALBASTAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Night radio",
description: "male, middle-aged, low pitch, slow and warm",
}),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const voice = await res.json();
console.log(voice.id, voice.status);Response
202 with the voice object, category designed and status pending.
{
"id": "vo_s683xywhmvVR5pbv2tJNtn",
"object": "voice",
"name": "Night radio",
"description": "male, middle-aged, low pitch, slow and warm",
"category": "designed",
"visibility": "private",
"owner": "you",
"status": "pending",
"error": null,
"duration_s": null,
"preview_url": null,
"preview_job": "mj_7tKcQ2vWn9ZxR4pLm8YbHd",
"audio_url": null,
"default_model": "omnivoice",
"usage_count": 0
}The response above leaves out the empty fields, transcript, models and the times.
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | bad-param | A field is missing, unknown or too long, or model does not design voices; param names the field |
| 400 | bad-json | The body is not a JSON object |
| 422 | voice_quota | The account already has quota voices, 30 by default |
| 429 | budget-exhausted | The account's daily generation time is used up; resets_at is the next 00:00 UTC |
| 429 | too-many-open | The key has 1,000 jobs queued or running, the limit per key |
Get a voice
GET /v1/media/voices/{id} returns a saved voice of the account, or a shared voice.
Parameters
| Name | Type | Description |
|---|---|---|
id required | string | The voice id vo_…, in the path |
Request
curl
curl https://api.albastov.com/v1/media/voices/vo_tBeDCRb2byGFjB6HGyQq6a \
-H "Authorization: Bearer $ALBASTAR_API_KEY"Python
import json
import os
import urllib.request
req = urllib.request.Request(
"https://api.albastov.com/v1/media/voices/vo_tBeDCRb2byGFjB6HGyQq6a",
headers={
"User-Agent": "my-app/1.0",
"Authorization": f"Bearer {os.environ['ALBASTAR_API_KEY']}",
},
)
with urllib.request.urlopen(req, timeout=30) as res:
voice = json.load(res)
print(voice["name"], voice["status"], voice["default_model"])JavaScript
const res = await fetch(
"https://api.albastov.com/v1/media/voices/vo_tBeDCRb2byGFjB6HGyQq6a",
{ headers: { Authorization: `Bearer ${process.env.ALBASTAR_API_KEY}` } },
);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const voice = await res.json();
console.log(voice.name, voice.status, voice.default_model);Response
{
"id": "vo_tBeDCRb2byGFjB6HGyQq6a",
"object": "voice",
"name": "Narrator A",
"description": "A synthetic voice: the Kokoro built-in voice af_heart. …",
"category": "cloned",
"visibility": "public",
"owner": "albastar",
"status": "ready",
"error": null,
"language": "en-us",
"gender": "female",
"duration_s": 14.04,
"transcript": "Okay, so here's the plan. We land just after sunset, …",
"preview_url": "…/v1/media/voices/vo_tBeDCRb2byGFjB6HGyQq6a/preview",
"audio_url": "…/v1/media/voices/vo_tBeDCRb2byGFjB6HGyQq6a/audio",
"default_model": "omnivoice"
}The response above leaves out the empty fields, models, usage_count and the times, shortens description and
transcript to their first words, and shortens each URL to its path; the URLs start with https://api.albastov.com.
consent is absent on a voice of another account.
Errors
| Status | Code | Meaning |
|---|---|---|
| 404 | voice_not_found | No saved voice with the id, or a private voice of another account; a built-in voice has no object of its own |
Update a voice
PATCH /v1/media/voices/{id} changes a voice of the account and returns the changed voice.
Parameters
A JSON body with the fields to change. An unknown field fails with 400 bad-param.
| Name | Type | Description |
|---|---|---|
id required | string | The voice id vo_…, in the path |
name | string | 1 to 100 characters |
description | string | 500 characters at most |
language, gender, accent | string | 35, 40 and 60 characters at most |
labels | object | Replaces the labels: up to 10 keys of 1 to 40 characters, string values of 100 at most |
transcript | string | The words of the clip, 5,000 characters at most |
consent | string | The consent note, 2,000 characters at most |
Request
curl
curl -X PATCH \
https://api.albastov.com/v1/media/voices/vo_FLUWnoRjtFXyrBE8uWoTxL \
-H "Authorization: Bearer $ALBASTAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"description": "Calm narrator for chapter readings",
"labels": {"book": "harbour"}}'Python
import json
import os
import urllib.request
body = {
"description": "Calm narrator for chapter readings",
"labels": {"book": "harbour"},
}
req = urllib.request.Request(
"https://api.albastov.com/v1/media/voices/vo_FLUWnoRjtFXyrBE8uWoTxL",
method="PATCH",
data=json.dumps(body).encode(),
headers={
"User-Agent": "my-app/1.0",
"Authorization": f"Bearer {os.environ['ALBASTAR_API_KEY']}",
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(req, timeout=30) as res:
print(json.load(res)["labels"])JavaScript
const res = await fetch(
"https://api.albastov.com/v1/media/voices/vo_FLUWnoRjtFXyrBE8uWoTxL",
{
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.ALBASTAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
description: "Calm narrator for chapter readings",
labels: { book: "harbour" },
}),
},
);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log((await res.json()).labels);Response
200 with the voice object, as in Create a voice.
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | bad-param | A field is unknown, not a string or too long; param names the field |
| 400 | bad-json | The body is not a JSON object |
| 404 | voice_not_found | No voice with the id on the key's account |
Delete a voice
DELETE /v1/media/voices/{id} deletes a voice of the account with its clip and preview. A job already queued with
the voice keeps its own copy of the clip.
Parameters
| Name | Type | Description |
|---|---|---|
id required | string | The voice id vo_…, in the path |
Request
curl
curl -X DELETE \
https://api.albastov.com/v1/media/voices/vo_FLUWnoRjtFXyrBE8uWoTxL \
-H "Authorization: Bearer $ALBASTAR_API_KEY"Python
import os
import urllib.request
req = urllib.request.Request(
"https://api.albastov.com/v1/media/voices/vo_FLUWnoRjtFXyrBE8uWoTxL",
method="DELETE",
headers={
"User-Agent": "my-app/1.0",
"Authorization": f"Bearer {os.environ['ALBASTAR_API_KEY']}",
},
)
with urllib.request.urlopen(req, timeout=30) as res:
print(res.status)JavaScript
const res = await fetch(
"https://api.albastov.com/v1/media/voices/vo_FLUWnoRjtFXyrBE8uWoTxL",
{
method: "DELETE",
headers: { Authorization: `Bearer ${process.env.ALBASTAR_API_KEY}` },
},
);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(res.status);Response
204 with an empty body.
Errors
| Status | Code | Meaning |
|---|---|---|
| 404 | voice_not_found | No voice with the id on the key's account |
Get the voice clip
GET /v1/media/voices/{id}/audio returns the voice's reference clip as saved.
Parameters
| Name | Type | Description |
|---|---|---|
id required | string | The voice id vo_…, in the path |
Headers
| Name | Description |
|---|---|
Range | Request. A byte range, such as bytes=0-1023; the answer is 206 with Content-Range |
Content-Type | Response. The clip's audio type, such as audio/mpeg or audio/wav |
Cache-Control | Response. public, max-age=86400 for a voice with visibility public; private, max-age=300 otherwise |
Request
curl
curl \
https://api.albastov.com/v1/media/voices/vo_tBeDCRb2byGFjB6HGyQq6a/audio \
-H "Authorization: Bearer $ALBASTAR_API_KEY" \
--output voice.mp3Python
import os
import urllib.request
req = urllib.request.Request(
"https://api.albastov.com/v1/media/voices/"
"vo_tBeDCRb2byGFjB6HGyQq6a/audio",
headers={
"User-Agent": "my-app/1.0",
"Authorization": f"Bearer {os.environ['ALBASTAR_API_KEY']}",
},
)
with urllib.request.urlopen(req, timeout=30) as res:
with open("voice.mp3", "wb") as f:
f.write(res.read())JavaScript
import { writeFile } from "node:fs/promises";
const res = await fetch(
"https://api.albastov.com/v1/media/voices/vo_tBeDCRb2byGFjB6HGyQq6a/audio",
{ headers: { Authorization: `Bearer ${process.env.ALBASTAR_API_KEY}` } },
);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
await writeFile("voice.mp3", Buffer.from(await res.arrayBuffer()));Response
200 with the clip as the body; 206 for a byte range.
Errors
| Status | Code | Meaning |
|---|---|---|
| 404 | voice_not_found | No saved voice with the id, or a private voice of another account |
| 409 | voice_not_ready | A designed voice still pending, or failed; status says which |
Get the voice preview
GET /v1/media/voices/{id}/preview returns a short line spoken in the voice by its default model. The preview of a
designed voice, and of Narrator A, is its reference clip.
Parameters
| Name | Type | Description |
|---|---|---|
id required | string | The voice id vo_…, in the path |
Headers
| Name | Description |
|---|---|
Range | Request. A byte range, such as bytes=0-1023; the answer is 206 with Content-Range |
Content-Type | Response. The preview's audio type, such as audio/mpeg |
Cache-Control | Response. public, max-age=86400 for a voice with visibility public; private, max-age=300 otherwise |
Request
curl
curl \
https://api.albastov.com/v1/media/voices/vo_tBeDCRb2byGFjB6HGyQq6a/preview \
-H "Authorization: Bearer $ALBASTAR_API_KEY" \
--output preview.mp3Python
import os
import urllib.request
req = urllib.request.Request(
"https://api.albastov.com/v1/media/voices/"
"vo_tBeDCRb2byGFjB6HGyQq6a/preview",
headers={
"User-Agent": "my-app/1.0",
"Authorization": f"Bearer {os.environ['ALBASTAR_API_KEY']}",
},
)
with urllib.request.urlopen(req, timeout=30) as res:
with open("preview.mp3", "wb") as f:
f.write(res.read())JavaScript
import { writeFile } from "node:fs/promises";
const base = "https://api.albastov.com/v1/media/voices";
const res = await fetch(`${base}/vo_tBeDCRb2byGFjB6HGyQq6a/preview`, {
headers: { Authorization: `Bearer ${process.env.ALBASTAR_API_KEY}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
await writeFile("preview.mp3", Buffer.from(await res.arrayBuffer()));Response
200 with the preview as the body; 206 for a byte range.
Errors
| Status | Code | Meaning |
|---|---|---|
| 404 | voice_not_found | No saved voice with the id, or a private voice of another account |
| 404 | preview_not_ready | The preview is not made yet; preview_url is set once the preview exists |
Copy a voice
POST /v1/media/voices/{id}/copy makes the account's own private copy of a voice the key may use: a new id, the
clip and preview copied, the category kept, the consent note left behind. The copy counts toward the voice quota.
Parameters
A JSON body, or no body.
| Name | Type | Description |
|---|---|---|
id required | string | The voice id vo_… to copy, in the path |
name | string | The copy's name, 1 to 100 characters; default the source voice's name |
Request
curl
curl -X POST \
https://api.albastov.com/v1/media/voices/vo_tBeDCRb2byGFjB6HGyQq6a/copy \
-H "Authorization: Bearer $ALBASTAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "My narrator"}'Python
import json
import os
import urllib.request
req = urllib.request.Request(
"https://api.albastov.com/v1/media/voices/"
"vo_tBeDCRb2byGFjB6HGyQq6a/copy",
data=json.dumps({"name": "My narrator"}).encode(),
headers={
"User-Agent": "my-app/1.0",
"Authorization": f"Bearer {os.environ['ALBASTAR_API_KEY']}",
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(req, timeout=30) as res:
print(json.load(res)["id"])JavaScript
const base = "https://api.albastov.com/v1/media/voices";
const res = await fetch(`${base}/vo_tBeDCRb2byGFjB6HGyQq6a/copy`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ALBASTAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "My narrator" }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log((await res.json()).id);Response
201 with the new voice object: owner you, visibility private, usage_count 0.
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | bad-param | A field other than name, or a name outside 1 to 100 characters |
| 400 | bad-json | The body is not a JSON object |
| 404 | voice_not_found | No saved voice with the id, or a private voice of another account |
| 409 | voice_not_ready | A designed voice still pending, or failed |
| 422 | voice_quota | The account already has quota voices, 30 by default |