Skip to content

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

0:00 / 0:14

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

0:00 / 0:11

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);
Response
{
  "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.

Shell
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.

Shell
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:

Response
{
  "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:

Output
vo_s683xywhmvVR5pbv2tJNtn ready

List 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=5

Python

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);
}
Response
{
  "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.

modelvoice
Left outA saved voice vo_…; the voice's default_model runs
kokoroA built-in voice; a saved voice fails with 422 voice_not_supported
omnivoice, qwen3-tts and the other cloning modelsA saved voice or another voice reference

The example speaks one line in Narrator A, the shared voice vo_tBeDCRb2byGFjB6HGyQq6a, on two models.

Shell
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"
done

Copy 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.

Shell
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.

Shell
curl -X DELETE https://api.albastov.com/v1/media/voices/vo_your_voice_id \
  -H "Authorization: Bearer $ALBASTAR_API_KEY"

Reference