Kolbo.AIKolbo.AI Docs
Developer API

Speech & Sound

Text-to-speech and sound effect generation with the Kolbo API.

Convert text to speech or generate sound effects using ElevenLabs and other providers.

Model identifiers are Kolbo-specific — always fetch available models from GET /api/v1/models?type=text_to_speech (or type=text_to_sound) before specifying a model. The legacy aliases type=speech / type=sound also work. Omitting model uses the endpoint default (eleven_v3 for speech, elevenlabs-sound-effects-v1 for sound); passing auto, smart-select, smart_select, smartselect, or auto-select resolves to the same default. model must be a string — an array returns 400 INVALID_MODEL.

All generation endpoints accept an optional project_id body field that routes the output into a specific project. See Projects.

List Voices

Discover available voices before generating speech. Returns both platform preset voices and your custom cloned/designed voices.

Endpoint

GET /api/v1/voices

Query Parameters

All three filters are case-insensitive exact matches (not substring matches), applied in memory.

ParameterTypeRequiredDescription
providerstringNoFilter by provider, e.g. elevenlabs, google. Applies to both preset and custom voices
languagestringNoFilter by language name or language code, e.g. english or en-US. Preset voices only — custom voices are not filtered by language
genderstringNoFilter by gender, e.g. female, male. Preset voices only — custom voices always report gender: null and so are never excluded by this filter

language and gender filter the preset catalog only. Your custom cloned voices are always appended to the result regardless of those two filters.

Example

curl "https://api.kolbo.ai/api/v1/voices?gender=female" \
  -H "X-API-Key: kolbo_live_..."

Response

{
  "success": true,
  "voices": [
    {
      "voice_id": "EXAVITQu4vr4xnSDxMaL",
      "name": "Rachel",
      "provider": "elevenlabs",
      "language": "English",
      "language_code": "en-US",
      "gender": "female",
      "accent": "American",
      "preview_url": "https://...",
      "thumbnail": "https://...",
      "description": "Calm, conversational narrator",
      "styles": ["conversational", "calm"],
      "v3_optimized": true,
      "custom": false
    },
    {
      "voice_id": "custom_68f2c1a9b4e5d6f7a8b9c0d1",
      "name": "My Cloned Voice",
      "provider": "elevenlabs",
      "language": "auto",
      "language_code": null,
      "gender": null,
      "accent": null,
      "preview_url": null,
      "thumbnail": null,
      "description": null,
      "styles": [],
      "v3_optimized": false,
      "voice_type": "clone",
      "voice_model_id": null,
      "custom": true
    }
  ],
  "count": 152
}

Custom voices are returned only when their status is ready and they have not been deleted. voice_type and voice_model_id appear on custom entries only.

Use the voice_id from this response as the voice parameter in the speech endpoint. You can also pass a voice name and the API resolves it automatically — see Voice resolution.


Text to Speech

Endpoint

POST /api/v1/generate/speech

Request Body

FieldTypeRequiredDescription
textstringYesText to convert to speech. Must be a non-empty string, otherwise 400 INVALID_PROMPT
voicestringNoVoice ID or display name from GET /api/v1/voices (default: "Rachel"). Required when model is cartesia_sonic_3_5 — omitting it returns 400 VOICE_REQUIRED
modelstringNoTTS model identifier from GET /api/v1/models?type=text_to_speech (default: eleven_v3). Advisory — for catalog voices the synthesis model comes from the voice record, not from this field. See the callout below
languagestringNoLanguage code, e.g. "en-US", "he-IL" (default: "en-US"). Send "auto" to detect the language from text server-side. Full language names are also accepted and mapped to locales: Hebrew, English, French, Spanish, German, Italian, Arabic — and Multilingual, which maps to auto
style_instructionsstringNoGoogle / Gemini voices only. Free-form voice direction, e.g. "whisper conspiratorially". Truncated to the first 500 characters
speaking_speednumberNoSpeech speed on a 0.5 (slow) – 2.0 (fast) scale, 1.0 = normal. Never rejected — each provider remaps it: ElevenLabs onto its own 0.71.2 speed, Cartesia clamps to 0.61.5, Google/Gemini turns it into a natural-language direction
project_idstringNoRoute the output into a specific project. Omit for the default "API Generations" project

model does not pick the synthesis engine for catalog voices. Every voice in GET /api/v1/voices carries its own model, and the TTS engine uses that one. What model actually does is: scope name → id resolution to that model's voices (step 3 below), gate the two Cartesia guards (VOICE_REQUIRED / VOICE_NOT_FOUND), and get echoed back as model in the "generation started" response. For a custom_<id> cloned voice it is only a starting point — the stored voice's provider overrides it. The model on the completed status response is the model that actually ran, and it can differ from the one you sent.

Voice resolution

voice is resolved server-side before the request reaches the TTS engine:

  1. Values that look like a raw provider id pass through untouched — 15+ alphanumerics (ElevenLabs style) or a UUID (Cartesia style).
  2. A custom_<id> value (a cloned voice from GET /v1/voices) also passes through untouched.
  3. Anything else is treated as a display name and looked up case-insensitively: first in the global catalog scoped to the model you passed, then in the global catalog unscoped, then among your own cloned voices (resolving to custom_<id>).
  4. If no match is found, the string is passed through unchanged. A dna_<visualDnaId> value (a Visual DNA character voice) is resolved at this point by the TTS engine. Anything else that is not a real catalog voice_id fails there with 400 Voice "<value>" not found.

Cartesia is strict: with model: "cartesia_sonic_3_5", a name that does not match a Cartesia voice returns 400 VOICE_NOT_FOUND instead of silently falling back to an ElevenLabs voice. The check fires before the custom-voice lookup in step 3, so a cloned Cartesia voice cannot be referenced by name on this model — pass its custom_<id> from GET /v1/voices instead. Cartesia also caps text at 5,000 characters per request (TEXT_TOO_LONG), measured against the trimmed text of the voice's resolved model. Pass the model alongside the voice name whenever you rely on name resolution, so a name that exists on several providers resolves to the one you meant.

VOICE_REQUIRED and VOICE_NOT_FOUND are emitted before the SDK response wrapper is installed, so their bodies are { "status": false, "code": ..., "message": ... } — not the usual { "success": false, "error": ..., "code": ... }. Branch on the HTTP status and code, and read the text from message as well as error.

Expressive & style controls (optional, provider-specific)

Each voice belongs to a provider; the engine ignores any control that does not apply to the chosen voice's provider, so you can pass only what you need.

FieldTypeProviderDescription
selected_stylestringDeepDub / MiniMaxPreset delivery style. DeepDub accepts exactly reading, angry, breathy, panic, amused, sad, whisper, singing, shout, scream, mumbling — anything else silently falls back to reading. On MiniMax voices it is used as the emotion source when emotion is not sent
emotionstringMiniMax / CartesiaMiniMax: happy, sad, angry, fearful, disgusted, surprised, calm, fluent, whisper — an unrecognised value falls back to auto-detect. Cartesia: neutral, calm, content, happy, excited, sad, angry, scared — anything else is rejected with INVALID_EMOTION
similarity_boostnumberElevenLabsVoice similarity 01 (default 0.75). Outside the range returns 400
stylenumberElevenLabsStyle exaggeration 01 (default 0.5). Outside the range returns 400
variancenumberDeepDubVoice variance 01. Outside the range returns 400
temponumberDeepDubTempo multiplier 02. Outside the range returns 400
promptBoostbooleanDeepDubPrompt-fidelity boost
seednumberDeepDub / ZonosReproducibility seed
accentControlobjectDeepDubAccent steering { accentBaseLocale, accentLocale, accentRatio }. Both locales are required when the object is sent, and accentRatio must be 01, otherwise 400
voiceTitlestringDeepDubDisplay title for a custom/cloned voice
minimax_pitchnumberMiniMaxPitch -1212 (default 0)
minimax_volnumberMiniMaxVolume 010 (default 1)
minimax_intensitynumberMiniMaxVoice intensity
minimax_timbrenumberMiniMaxVoice timbre
style_instructions_labelstringGoogle / GeminiMetadata only — the label of the style preset the caller picked. Does not change the audio
style_instructions_preset_idstringGoogle / GeminiMetadata only — the id of the style preset the caller picked. Does not change the audio

The list above is an explicit allowlist: any other field in the request body is dropped before the TTS engine sees it.

Two exceptions worth knowing: use_speaker_boost is accepted by the request layer but has no effect — the ElevenLabs path hardcodes speaker boost on. And stability cannot be sent directly; it is derived from speaking_speed.

Examples

Omit model to use the server default. You can use a voice name directly:

curl -X POST https://api.kolbo.ai/api/v1/generate/speech \
  -H "X-API-Key: kolbo_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Welcome to Kolbo AI, the all-in-one creative platform.",
    "voice": "Rachel"
  }'

With Specific Model

To choose a specific model, first fetch identifiers from GET /api/v1/models?type=text_to_speech, then pass the identifier value:

curl -X POST https://api.kolbo.ai/api/v1/generate/speech \
  -H "X-API-Key: kolbo_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Welcome to Kolbo AI, the all-in-one creative platform.",
    "voice": "Rachel",
    "model": "eleven_v3"
  }'

With Language

curl -X POST https://api.kolbo.ai/api/v1/generate/speech \
  -H "X-API-Key: kolbo_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Welcome to Kolbo AI, the all-in-one creative platform.",
    "voice": "Rachel",
    "language": "en-US"
  }'

Response

POST /v1/generate/speech is asynchronous and fire-and-forget: it answers 202 Accepted as soon as the job is queued and never contains audio.

Polling is the only completion mechanism. Kolbo never calls you back: there is no webhook, no callback_url field on this request body, and no route delivers outbound notifications. Socket.IO events are the web app's internal transport and are not part of the API contract — API-key generations are registered so the shared progress emitter drops their events, and the few emit sites that bypass that check are undocumented, unversioned and unsafe to build on. After the POST, loop against GET /api/v1/generate/{generation_id}/status until state is completed, failed or cancelled. Full contract: Polling & Cancellation.

Generation Started

{
  "success": true,
  "generation_id": "68f2c1a9b4e5d6f7a8b9c0d1",
  "type": "speech",
  "model": "eleven_v3",
  "credits_charged": 5,
  "poll_url": "/v1/generate/68f2c1a9b4e5d6f7a8b9c0d1/status",
  "poll_interval_hint": 8,
  "session_id": "68f2c1a9b4e5d6f7a8b9c0d2",
  "project_id": "68f2c1a9b4e5d6f7a8b9c0d3"
}
FieldTypeNotes
generation_idstringMongo ObjectId. This is what you poll and cancel with.
typestringAlways "speech"
modelstringThe identifier you sent, or the default eleven_v3
credits_chargednumberSpeech is one of only two endpoints that return a real estimate here — the character-based pre-calculation. Nothing is charged yet: credits are deducted only after a successful synthesis, and the authoritative figure is credits_used on the completed status.
poll_urlstringThe status path without the /api prefix. Prepend https://api.kolbo.ai/api.
poll_interval_hintnumberSuggested seconds between polls — 8 for speech
session_id / project_idstringWhere the generation lives in the Kolbo app

Completed Status

GET /api/v1/generate/{generation_id}/status

{
  "success": true,
  "generation_id": "68f2c1a9b4e5d6f7a8b9c0d1",
  "type": "speech",
  "state": "completed",
  "progress": 100,
  "result": {
    "urls": ["https://media.kolbo.ai/tts/.../speech.mp3"],
    "voice": "EXAVITQu4vr4xnSDxMaL",
    "duration": null,
    "model": "eleven_v3",
    "created_at": "2026-07-20T14:20:00Z"
  },
  "credits_used": 6,
  "credits_breakdown": [
    { "model": "eleven_v3", "amount": 6, "base": 6, "final": 6, "duration_multiplier": null, "pricing": null }
  ]
}

The output lives in result.urls — always an array of strings with exactly one entry. Read urls[0].

result (and credits_used / credits_breakdown) appear only under state: "completed"; a processing body has exactly five keys and no result at all. There is no partial or streaming read.

result.voice is the resolved voice id, not the name you sent. result.model is the model that actually synthesised the audio — for a catalog voice this is the voice's own model, which may differ from the model echoed in the "generation started" response. result.duration is null in practice on this path: the TTS record does not persist an output length, so measure the file yourself if you need it.

Failure and cancellation

A failed synthesis is still an HTTP 200 with success: true — the failure is in state. Note that on this endpoint the failure writer stores no message, so error reads the generic "Generation failed" and every failure.* sub-field is null; the one exception is the 15-minute server timeout, which does report a real message. cancelled carries neither result nor error, so treat all three terminal states explicitly. Credits are deducted only on success, so a failed synthesis costs nothing. See Polling & Cancellation.

JavaScript Example

const KOLBO_API_KEY = "kolbo_live_..."; // Replace with your API key
const BASE_URL = "https://api.kolbo.ai/api";

async function generateSpeech() {
  const response = await fetch(`${BASE_URL}/v1/generate/speech`, {
    method: "POST",
    headers: {
      "X-API-Key": KOLBO_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      text: "Welcome to Kolbo AI, the all-in-one creative platform.",
      voice: "Rachel",
    }),
  });

  const started = await response.json();
  if (!started.success) throw new Error(started.error);

  const result = await pollUntilDone(started.generation_id, started.poll_interval_hint);
  if (result.state !== "completed") throw new Error(result.error || result.state);

  console.log("Audio URL:", result.result.urls[0]); // urls is always an array
  return result.result;
}

const TERMINAL = new Set(["completed", "failed", "cancelled"]);

// Minimal loop. A production client should also handle transient HTTP errors and
// an overall timeout — see /docs/developer-api/polling-and-cancellation.
async function pollUntilDone(generationId, intervalSeconds) {
  while (true) {
    await new Promise((r) => setTimeout(r, intervalSeconds * 1000));
    const status = await fetch(
      `${BASE_URL}/v1/generate/${generationId}/status`,
      { headers: { "X-API-Key": KOLBO_API_KEY } }
    ).then((r) => r.json());
    if (TERMINAL.has(status.state)) return status;
  }
}

generateSpeech().catch(console.error);

Python Example

import requests
import time

KOLBO_API_KEY = "kolbo_live_..."  # Replace with your API key
BASE_URL = "https://api.kolbo.ai/api"

response = requests.post(
    f"{BASE_URL}/v1/generate/speech",
    headers={"X-API-Key": KOLBO_API_KEY},
    json={
        "text": "Welcome to Kolbo AI, the all-in-one creative platform.",
        "voice": "Rachel",
    },
)
started = response.json()
if not started.get("success"):
    raise Exception(started.get("error", "Request failed"))

TERMINAL = {"completed", "failed", "cancelled"}


def poll_until_done(generation_id, interval_seconds):
    """Minimal loop. Add transient-error retries and an overall timeout for
    production — see /docs/developer-api/polling-and-cancellation."""
    while True:
        time.sleep(interval_seconds)
        status = requests.get(
            f"{BASE_URL}/v1/generate/{generation_id}/status",
            headers={"X-API-Key": KOLBO_API_KEY},
        ).json()
        if status["state"] in TERMINAL:
            return status


result = poll_until_done(started["generation_id"], started["poll_interval_hint"])
if result["state"] != "completed":
    raise Exception(result.get("error", result["state"]))

print("Audio URL:", result["result"]["urls"][0])  # urls is always a list

Credits

Speech credits are character-based on the trimmed text: ceil(text.trim().length / 100) x model.credit, where model.credit is the credit field from GET /api/v1/models?type=text_to_speech.

For example, a 250-character text with a model that costs 1 credit per 100 chars: ceil(250 / 100) x 1 = 3 credits.


Sound Effects

Endpoint

POST /api/v1/generate/sound

Request Body

FieldTypeRequiredDescription
promptstringYesDescription of the sound effect. Must be a non-empty string, otherwise 400 INVALID_PROMPT
modelstringNoModel identifier from GET /api/v1/models?type=text_to_sound (default: elevenlabs-sound-effects-v1)
durationnumberNoDuration in seconds. Omit for automatic duration. Not clamped server-side — the value is forwarded to the provider as-is, so an out-of-range request fails at the provider. Read min_output_duration / max_output_duration / default_duration from the model entry in GET /api/v1/models?type=text_to_sound to stay inside the model's range. On per-second-billed models, omitting it makes the credit pre-check assume default_duration (falling back to 8s)
prompt_influencenumberNoElevenLabs: prompt guidance 01 (default 0.5). Higher = more literal
project_idstringNoRoute the output into a specific project. Omit for the default "API Generations" project

Advanced controls (optional, provider-specific)

The engine ignores any control that does not apply to the chosen model.

FieldTypeProviderDescription
cfg_strengthnumberFAL Stable Audio / mmaudioClassifier-free guidance strength
sound_loopbooleanKieGenerate a seamlessly looping sound
sound_temponumberKieTempo control
sound_keystringKieMusical key / scale
seed_voicestringFAL Seed-AudioVoice to use
seed_speednumberFAL Seed-AudioSpeed multiplier 0.52.0
seed_volumenumberFAL Seed-AudioVolume 01
seed_pitchnumberFAL Seed-AudioPitch shift in semitones
seed_reference_audio_urlsstring[]FAL Seed-AudioReference audio URLs. Only the first 3 are forwarded; the rest are dropped
seed_reference_image_urlstringFAL Seed-AudioReference image URL
audio_formatstringSoniloOutput container/codec. The accepted set is fixed: wav | mp3 | aac | flac. An unrecognised value is not an error — it silently falls back to default_output_audio_format (or aac). Read entirely only on models that advertise a non-empty output_audio_formats; ignored on every other model

Examples

curl -X POST https://api.kolbo.ai/api/v1/generate/sound \
  -H "X-API-Key: kolbo_live_..." \
  -H "Content-Type: application/json" \
  -d '{"prompt": "Thunder clap followed by heavy rain"}'

With Specific Model

To choose a specific model, first fetch identifiers from GET /api/v1/models?type=text_to_sound, then pass the identifier value:

curl -X POST https://api.kolbo.ai/api/v1/generate/sound \
  -H "X-API-Key: kolbo_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Thunder clap followed by heavy rain",
    "model": "elevenlabs-sound-effects-v1"
  }'

With Duration

curl -X POST https://api.kolbo.ai/api/v1/generate/sound \
  -H "X-API-Key: kolbo_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Gentle ocean waves on a sandy beach",
    "duration": 10
  }'

Response

POST /v1/generate/sound is asynchronous and fire-and-forget: it answers 202 Accepted as soon as the job is queued and never contains audio.

Polling is the only completion mechanism. Kolbo never calls you back: there is no webhook, no callback_url field on this request body, and no route delivers outbound notifications. Socket.IO events are the web app's internal transport and are not part of the API contract — API-key generations are registered so the shared progress emitter drops their events, and the few emit sites that bypass that check are undocumented, unversioned and unsafe to build on. After the POST, loop against GET /api/v1/generate/{generation_id}/status until state is completed, failed or cancelled. Full contract: Polling & Cancellation.

Generation Started

{
  "success": true,
  "generation_id": "68f2c1a9b4e5d6f7a8b9c0d1",
  "type": "sound",
  "model": "elevenlabs-sound-effects-v1",
  "credits_charged": 7,
  "poll_url": "/v1/generate/68f2c1a9b4e5d6f7a8b9c0d1/status",
  "poll_interval_hint": 5,
  "session_id": "68f2c1a9b4e5d6f7a8b9c0d2",
  "project_id": "68f2c1a9b4e5d6f7a8b9c0d3"
}
FieldTypeNotes
generation_idstringMongo ObjectId. This is what you poll and cancel with.
typestringAlways "sound"
modelstringThe identifier you sent, or the default elevenlabs-sound-effects-v1
credits_chargednumberSound is one of only two endpoints that return a real estimate here. Nothing is charged yet: credits are deducted only after a successful generation, and the authoritative figure is credits_used on the completed status.
poll_urlstringThe status path without the /api prefix. Prepend https://api.kolbo.ai/api.
poll_interval_hintnumberSuggested seconds between polls — 5 for sound, the only endpoint that uses this value
session_id / project_idstringWhere the generation lives in the Kolbo app

Completed Status

GET /api/v1/generate/{generation_id}/status

{
  "success": true,
  "generation_id": "68f2c1a9b4e5d6f7a8b9c0d1",
  "type": "sound",
  "state": "completed",
  "progress": 100,
  "result": {
    "urls": ["https://media.kolbo.ai/sound/.../sfx.mp3"],
    "duration": 8,
    "prompt_used": "Thunder clap followed by heavy rain",
    "model": "elevenlabs-sound-effects-v1",
    "created_at": "2026-07-20T14:20:00Z"
  },
  "credits_used": 4,
  "credits_breakdown": [
    { "model": "elevenlabs-sound-effects-v1", "amount": 4, "base": 4, "final": 4, "duration_multiplier": null, "pricing": null }
  ]
}

The output lives in result.urls — always an array of strings with exactly one entry. Read urls[0].

result (and credits_used / credits_breakdown) appear only under state: "completed"; a processing body has exactly five keys and no result at all.

result.duration is the length you requested, not the measured length of the file — when you omit duration (auto mode) it is null. result.prompt_used is your original prompt, before any translation.

Failure and cancellation

A failed generation is still an HTTP 200 with success: true — the failure is in state, with the provider's message in error and failure.message. failure.code carries the provider error code or the literal GENERATION_ERROR (GENERATION_TIMEOUT on the 15-minute server deadline); failure.category, retryable, severity and provider are always null on this endpoint. cancelled carries neither result nor error, so treat all three terminal states explicitly. Credits are deducted only on success, so a failed generation costs nothing. See Polling & Cancellation.

JavaScript Example

const KOLBO_API_KEY = "kolbo_live_..."; // Replace with your API key
const BASE_URL = "https://api.kolbo.ai/api";

async function generateSound() {
  const response = await fetch(`${BASE_URL}/v1/generate/sound`, {
    method: "POST",
    headers: {
      "X-API-Key": KOLBO_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      prompt: "Thunder clap followed by heavy rain",
    }),
  });

  const started = await response.json();
  if (!started.success) throw new Error(started.error);

  // pollUntilDone() from the Text to Speech example above — same status route
  // and state machine, and poll_interval_hint is 5 seconds here instead of 8.
  const result = await pollUntilDone(started.generation_id, started.poll_interval_hint);
  if (result.state !== "completed") throw new Error(result.error || result.state);

  console.log("Sound URL:", result.result.urls[0]);
  return result.result;
}

generateSound().catch(console.error);

Python Example

import requests

KOLBO_API_KEY = "kolbo_live_..."  # Replace with your API key
BASE_URL = "https://api.kolbo.ai/api"

started = requests.post(
    f"{BASE_URL}/v1/generate/sound",
    headers={"X-API-Key": KOLBO_API_KEY},
    json={
        "prompt": "Thunder clap followed by heavy rain",
    },
).json()
if not started.get("success"):
    raise Exception(started.get("error", "Request failed"))

# poll_until_done() from the Text to Speech example above — same status route
# and state machine, and poll_interval_hint is 5 seconds here instead of 8.
result = poll_until_done(started["generation_id"], started["poll_interval_hint"])
if result["state"] != "completed":
    raise Exception(result.get("error", result["state"]))

print("Sound URL:", result["result"]["urls"][0])

Voices

Clone, import, and delete your own custom voices. Cloned voices appear in GET /v1/voices with custom: true and can be used with the speech endpoint.

These field names are camelCase, unlike the snake_case body fields on the generation endpoints. They are passed through to the underlying voice controller unchanged.

Clone a Voice

POST /api/v1/voices/clone

multipart/form-data only — the handler reads uploaded file buffers.

FieldTypeRequiredDescription
voiceNamestringYesName for the cloned voice. A blank/whitespace value returns 400
audioFilefileYesThe voice sample. Missing file returns 400. Route cap: 25 MB (multer memory storage)
providerstringNoOne of deepdub, elevenlabs, minimax, zonos, cartesia. Default: deepdub. Any other value returns 400
languagestringNoLanguage hint. Auto-detected when omitted
promptTextstringNoMiniMax clone_prompt text
promptAudioFilefileNoMiniMax clone_prompt reference audio
avatarFilefileNoAvatar image for the voice
project_idstringNoProject to associate the voice with. Omit for the default "API Generations" project

Sample duration limits (validated after decode, before cloning):

ProviderMinMaxNotes
elevenlabs2s180s
deepdub2s300sThe sample is trimmed to the first 6 seconds during processing
zonos1s60sAdditional 10 MB file cap
cartesia2s60sAdditional 10 MB file cap
minimax2sNo provider-specific duration ceiling

Files under 1 KB or with zero bytes are rejected as empty/corrupt; anything over 200 MB is rejected outright (the 25 MB route cap bites first on this endpoint). Cloning charges credits (provider-dependent).

curl -X POST https://api.kolbo.ai/api/v1/voices/clone \
  -H "X-API-Key: kolbo_live_..." \
  -F "[email protected]" \
  -F "voiceName=Narrator" \
  -F "provider=elevenlabs"
{
  "success": true,
  "voice": {
    "id": "",
    "name": "Narrator",
    "provider": "elevenlabs",
    "language": "english",
    "preview_audio_url": null
  }
}

The id and preview_audio_url fields in this response are currently always empty — the SDK response shaper reads keys the underlying voice controller does not return. To get the new voice's id, call GET /api/v1/voices after cloning and take the voice_id (already in custom_<id> form) of the entry whose name matches. You can also just pass the voice name to the speech endpoint.

Note that provider on the stored voice is the backend provider, not always what you sent: zonos is persisted as fal-zonos.

Import an ElevenLabs Voice

POST /api/v1/voices/import-elevenlabs

Copies a voice from the ElevenLabs Voice Library into your Kolbo voices.

FieldTypeRequiredDescription
elevenLabsVoiceIdstringYesPresence is enforced by the SDK layer — omitting it returns 400
voiceIdstringYesThe ElevenLabs voice ID, or its share URL. This is the field the import actually reads. shareUrl or original_voice_id work as equivalents
voiceNamestringNoName for the imported voice. Falls back to the library's own name, then to Imported Voice <timestamp>
public_user_idstringNoThe upstream owner id. Resolved automatically from a share URL or a shared-voice lookup when omitted
project_idstringNoProject to associate the voice with

elevenLabsVoiceId gates the request but is not the field the importer reads. Send the id in both elevenLabsVoiceId and voiceId (or shareUrl) — with elevenLabsVoiceId alone the request fails with 400 INVALID_SHARE_URL.

Failure modes, by HTTP status: 400 no id readable (INVALID_SHARE_URL) or the voice is private / removed / not a public-library voice (VOICE_NOT_IMPORTABLE); 404 no public voice matches the id (VOICE_NOT_FOUND); 409 you already imported this voice (DUPLICATE_IMPORT) or the ElevenLabs library slot limit was reached (ELEVENLABS_SLOT_LIMIT); 502 ElevenLabs unreachable or returned no id (IMPORT_FAILED / IMPORT_NO_ID).

The /v1/voices/* endpoints return errors as { "success": false, "error": "<message>" } — the internal codes named above are not included in the SDK response body. Branch on the HTTP status and the message text, not on a code field.

On success the response is { "success": true, "voice": { "id": "", "name": ..., "provider": "elevenlabs-import", "elevenlabs_voice_id": null } }id and elevenlabs_voice_id are empty for the same shaper reason as the clone endpoint. Resolve the real id via GET /api/v1/voices.

curl -X POST https://api.kolbo.ai/api/v1/voices/import-elevenlabs \
  -H "X-API-Key: kolbo_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "elevenLabsVoiceId": "21m00Tcm4TlvDq8ikWAM",
    "voiceId": "21m00Tcm4TlvDq8ikWAM"
  }'

Delete a Voice

DELETE /api/v1/voices/:id

Soft-deletes one of your own custom voices (owner-scoped; returns 404 for a voice you do not own). Preset catalog voices cannot be deleted.

ParameterTypeRequiredDescription
idstring (path)YesThe raw Mongo ObjectId of the custom voice. Must be a valid ObjectId, otherwise 400 Invalid voice id.

GET /v1/voices returns custom voices as custom_<id>. Strip the custom_ prefix before calling this endpoint — passing the prefixed value returns 400 Invalid voice id.

curl -X DELETE https://api.kolbo.ai/api/v1/voices/68f2c1a9b4e5d6f7a8b9c0d1 \
  -H "X-API-Key: kolbo_live_..."

Rate Limits

EndpointLimit per account
GET /v1/voicesNo dedicated limiter
POST /v1/voices/clone10 / minute
POST /v1/voices/import-elevenlabs10 / minute
DELETE /v1/voices/:id120 / minute

MCP tools covering this surface: list_voices, clone_voice, import_elevenlabs_voice, delete_voice.

Tips

  • POST /v1/generate/speech and POST /v1/generate/sound share the standard SDK generation limit of 10 requests/minute per account
  • Poll at the cadence in poll_interval_hint — 8 seconds for speech, 5 for sound
  • Both endpoints return audio URLs on media.kolbo.ai that can be downloaded or streamed
  • Use GET /api/v1/voices to discover available voices before generating speech