Skip to content

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

FieldTypeValue
idstringvo_ and 22 random characters; a built-in voice keeps its name, such as af_heart
objectstringvoice
namestring1 to 100 characters
descriptionstring0 to 500 characters; the description of a designed voice
categorystringcloned (from a clip), designed (from a description) or builtin (a model's own voice)
visibilitystringprivate for a voice of the key's account; public or members for a shared voice, which every key may use
ownerstringyou for a voice of the key's account, albastar for a shared or built-in voice
statusstringready; pending while a designed voice is made; failed
errorstring or nullWhy a designed voice failed
languagestringA language code such as en or en-gb, set by the voice's account
gender, accentstringFree text set by the voice's account
labelsobjectUp to 10 string keys and string values, such as {"use": "narration"}
duration_snumber or nullThe clip's length; null for a WebM clip, a built-in voice and a designed voice still pending
transcriptstringThe words of the clip; filled on first use when empty
preview_urlstring or nullA short line spoken in the voice; null until made
preview_jobstring or nullThe job making the preview or the designed voice
audio_urlstring or nullThe reference clip; null for a built-in voice and a voice that is not ready
default_modelstring or nullFor 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
modelsarrayEvery model that speaks the voice, by model id
usage_countintegerJobs submitted with the voice, the preview excluded
created_at, updated_atstring or nullRFC 3339 times; null for a built-in voice
consentstringThe 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

NameTypeDescription
scopestringall (default): the account's voices and every shared voice; mine; shared
modelstringOnly 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
categorystringcloned, designed or builtin; builtin needs model
languagestringen matches en and every en-…
qstringText in the name or description, any case; for a built-in voice, text in the id
limitinteger1 to 100, default 20
cursorstringnext_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=5

Python

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

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

StatusCodeMeaning
400bad-paramscope, 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.

NameTypeDescription
file required in multipartfileWAV, 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 JSONstringAn upload of the account up_…, or a result of the account's job job:<id>/<i>
namestring1 to 100 characters. Required in JSON; in multipart, defaults to the file name without its extension
descriptionstring500 characters at most
languagestringA language code such as en, 35 characters at most
genderstring40 characters at most
accentstring60 characters at most
labelsobjectUp to 10 keys of 1 to 40 characters, string values of 100 at most; in multipart, the object as JSON text
transcriptstringThe words of the clip, 5,000 characters at most
consentstringWho 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.

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."}'

Response

201 with the voice object.

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

StatusCodeMeaning
400bad-paramA 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, ...)
400bad-uploadThe multipart body has no boundary or did not arrive whole
400bad-jsonThe JSON body is not an object
413too-largeThe clip is over 20 MB
415not-audioThe clip is not WAV, MP3, M4A, Ogg, WebM or FLAC
415multipart-or-jsonThe body is neither multipart nor JSON
422voice_quotaThe 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.

NameTypeDescription
name requiredstring1 to 100 characters
description requiredstringThe voice to make, such as "female, warm, low pitch, slow", 500 characters at most
modelstringomnivoice (default)
textstringThe line the voice speaks in its clip, 1,000 characters at most; default a fixed passage of about 15 s
languagestringA 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.

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,
  "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

StatusCodeMeaning
400bad-paramA field is missing, unknown or too long, or model does not design voices; param names the field
400bad-jsonThe body is not a JSON object
422voice_quotaThe account already has quota voices, 30 by default
429budget-exhaustedThe account's daily generation time is used up; resets_at is the next 00:00 UTC
429too-many-openThe 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

NameTypeDescription
id requiredstringThe 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

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

StatusCodeMeaning
404voice_not_foundNo 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.

NameTypeDescription
id requiredstringThe voice id vo_…, in the path
namestring1 to 100 characters
descriptionstring500 characters at most
language, gender, accentstring35, 40 and 60 characters at most
labelsobjectReplaces the labels: up to 10 keys of 1 to 40 characters, string values of 100 at most
transcriptstringThe words of the clip, 5,000 characters at most
consentstringThe 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

StatusCodeMeaning
400bad-paramA field is unknown, not a string or too long; param names the field
400bad-jsonThe body is not a JSON object
404voice_not_foundNo 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

NameTypeDescription
id requiredstringThe 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

StatusCodeMeaning
404voice_not_foundNo 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

NameTypeDescription
id requiredstringThe voice id vo_…, in the path

Headers

NameDescription
RangeRequest. A byte range, such as bytes=0-1023; the answer is 206 with Content-Range
Content-TypeResponse. The clip's audio type, such as audio/mpeg or audio/wav
Cache-ControlResponse. 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.mp3

Python

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

StatusCodeMeaning
404voice_not_foundNo saved voice with the id, or a private voice of another account
409voice_not_readyA 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

NameTypeDescription
id requiredstringThe voice id vo_…, in the path

Headers

NameDescription
RangeRequest. A byte range, such as bytes=0-1023; the answer is 206 with Content-Range
Content-TypeResponse. The preview's audio type, such as audio/mpeg
Cache-ControlResponse. 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.mp3

Python

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

StatusCodeMeaning
404voice_not_foundNo saved voice with the id, or a private voice of another account
404preview_not_readyThe 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.

NameTypeDescription
id requiredstringThe voice id vo_… to copy, in the path
namestringThe 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

StatusCodeMeaning
400bad-paramA field other than name, or a name outside 1 to 100 characters
400bad-jsonThe body is not a JSON object
404voice_not_foundNo saved voice with the id, or a private voice of another account
409voice_not_readyA designed voice still pending, or failed
422voice_quotaThe account already has quota voices, 30 by default