Voices
Clone, design, list, copy and delete saved voices, and speak any text in a saved voice.
Access: Keys with the media:read or media:generate scope, on an account with media access granted by Albastar Corp.
A saved voice is a reference clip of one voice with an id vo_…; a cloning model speaks any text in the voice.
Clone from a clip
POST /v1/media/voices saves a clip of 3 to 60 s as a voice. The example downloads the reference clip of Narrator A,
a synthetic voice made with Kokoro's built-in voice af_heart, saves the clip as a voice of the account, and prints
the voice id. A recording of a real speaker works the same way:
Voice cloning covers the clip and the consent note.
The reference clip, and omnivoice speaking a new line in the voice of the clip:
ReferenceNarrator A · synthetic
Transcript Okay, so here's the plan. We land just after sunset, grab the bags, and head straight for the harbour. If the ferry's late again — and honestly, it usually is — we'll get dinner by the water and wait it out. Sound good?
CloneOmniVoice in the reference voice
Transcript I found the photos from the lake house. Do you remember that summer? We stayed up until three, arguing about the stars, and you swore you'd seen a comet. I still think it was a plane.
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 consent="Synthetic voice made with the Kokoro voice af_heart. No speaker."Python
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",
"consent": "Synthetic voice made with the Kokoro voice af_heart. "
"No speaker.",
},
timeout=60,
)
res.raise_for_status()
voice = res.json()
print(voice["id"], voice["default_model"])JavaScript
const base = "https://api.albastov.com/v1";
const auth = { Authorization: `Bearer ${process.env.ALBASTAR_API_KEY}` };
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(
"consent",
"Synthetic voice made with the Kokoro voice af_heart. No speaker.",
);
const res = await fetch(`${base}/media/voices`, {
method: "POST",
headers: auth,
body: form,
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const voice = await res.json();
console.log(voice.id, voice.default_model);{
"id": "vo_FLUWnoRjtFXyrBE8uWoTxL",
"object": "voice",
"name": "Narrator A",
"category": "cloned",
"visibility": "private",
"owner": "you",
"status": "ready",
"error": null,
"language": "en",
"duration_s": 14.04,
"preview_url": null,
"preview_job": "mj_uyeqTjDrzLxNLPpBKuibEi",
"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 leaves out the empty fields (description, gender, accent, labels, transcript) and
audio_url, which is https://api.albastov.com/v1/media/voices/<id>/audio. A cloned voice is ready at once.
preview_url is null until the preview, one sentence spoken in the voice, is made. models lists every model that
speaks the voice, by model id; default_model runs when a request leaves model out.
Save an upload or a job result
A clip already on the account needs no second upload: send JSON with from set to an upload up_… or a job result
job:<id>/<i> instead of the multipart 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."}'The response is the voice object, as above. Uploads: Uploads reference.
Design from a description
POST /v1/media/voices/design makes a synthetic voice from a description, with no speaker behind the voice. text
sets the line the voice speaks in its reference clip, 1,000 characters at most; without text, the voice speaks a
fixed passage of about 15 s. The answer is 202 with status pending; GET /v1/media/voices/{id} returns
status ready when the voice is made, or failed with error.
VOICE=$(curl -s 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",
"text": "Good evening, and welcome to the night shift."}' \
| python3 -c 'import json, sys; print(json.load(sys.stdin)["id"])')
for i in $(seq 60); do
STATUS=$(curl -s "https://api.albastov.com/v1/media/voices/$VOICE" \
-H "Authorization: Bearer $ALBASTAR_API_KEY" \
| python3 -c 'import json, sys; print(json.load(sys.stdin)["status"])')
[ "$STATUS" != pending ] && break
sleep 2
done
echo "$VOICE $STATUS"The 202 response to the design request:
{
"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,
"transcript": "Good evening, and welcome to the night shift.",
"preview_url": null,
"preview_job": "mj_7tKcQ2vWn9ZxR4pLm8YbHd",
"audio_url": null,
"default_model": "omnivoice",
"usage_count": 0,
"created_at": "2026-11-03T04:10:02.511Z",
"updated_at": "2026-11-03T04:10:02.511Z"
}The response above leaves out the empty fields and models. The loop prints the voice id and its final status:
vo_s683xywhmvVR5pbv2tJNtn readyList voices
GET /v1/media/voices returns the voices the key may use, newest first: the account's own voices and every shared
voice. With model, the list holds the voices the model speaks, and the model's built-in voices come first.
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.request
req = urllib.request.Request(
"https://api.albastov.com/v1/media/voices"
"?model=kokoro&language=en-gb&limit=5",
headers={
# urllib's default User-Agent gets 403 with error code 1010.
"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"], v["language"])JavaScript
const res = await fetch(
"https://api.albastov.com/v1/media/voices" +
"?model=kokoro&language=en-gb&limit=5",
{ 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, v.language);
}{
"object": "list",
"data": [
{
"id": "bf_alice",
"object": "voice",
"name": "bf_alice",
"category": "builtin",
"visibility": "public",
"owner": "albastar",
"status": "ready",
"error": null,
"language": "en-gb",
"gender": "female",
"duration_s": null,
"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 the eight British English voices of Kokoro and leaves out the empty
fields. Built-in voices do not count toward limit, so all eight come back with limit=5; kokoro speaks no saved
voice, so the list holds no other voice. Filters and paging:
Voices reference.
Speak in a voice
voice in POST /v1/audio/speech takes the voice id. With a saved voice vo_…, model is optional: the voice's
default_model runs. Any model in the voice's models speaks the voice.
model | voice |
|---|---|
| Left out | A saved voice vo_…; the voice's default_model runs |
kokoro | A built-in voice; a saved voice fails with 422 voice_not_supported |
omnivoice, qwen3-tts and the other cloning models | A saved voice or another voice reference |
The example speaks one line in Narrator A, the shared voice vo_tBeDCRb2byGFjB6HGyQq6a, on two models.
VOICE=vo_tBeDCRb2byGFjB6HGyQq6a
for MODEL in omnivoice qwen3-tts; do
curl https://api.albastov.com/v1/audio/speech \
-H "Authorization: Bearer $ALBASTAR_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"model\": \"$MODEL\", \"voice\": \"$VOICE\",
\"input\": \"The next ferry leaves at a quarter past six.\"}" \
--output "ferry-$MODEL.mp3"
doneCopy a voice
POST /v1/media/voices/{id}/copy makes the account's own private copy of a voice the key may use, such as a shared
voice: a new id, the clip and preview copied, the category kept, the consent note left behind. The copy counts toward
the account's 30 voices and takes PATCH and DELETE like any voice of the account.
curl 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"}'Delete a voice
DELETE /v1/media/voices/{id} answers 204 and deletes the clip and the preview. Only the voice's own account
deletes a voice. A job already queued with the voice keeps its own copy of the clip: deleting the voice leaves the job
intact.
curl -X DELETE https://api.albastov.com/v1/media/voices/vo_your_voice_id \
-H "Authorization: Bearer $ALBASTAR_API_KEY"Reference
- Voices reference: the voice object, every route, parameter and error.
- Uploads reference:
POST /v1/media/uploadsforfrom. - Speech reference: speaking in a voice.