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_v4 for speech, elevenlabs-sound-effects-v1 for sound); passing an empty string, auto, smart-select, smart_select, smartselect, auto-select or kolbo_smart_select_router resolves to the same default. An identifier that does not match a catalog model is rejected with 400 MODEL_NOT_FOUND. Speech also accepts eleven_v4_turbo, which runs ElevenLabs V4 with turbo on. model must be a string — an array or object returns 400 INVALID_MODEL.

All generation endpoints accept an optional project_id body field that routes the output into a specific project, and an optional session_id that drops the generation into an existing session you can write to (it wins over project_id). 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 filters are case-insensitive and applied in memory. provider and gender are exact matches; language is a substring match.

ParameterTypeRequiredDescription
providerstringNoExact match on provider, e.g. elevenlabs, google. Applies to both preset and custom voices. Custom voices are matched against their stored backend provider (e.g. fal-zonos, elevenlabs-import)
languagestringNoSubstring match against the language name or language code — he, hebrew and he-IL all match Hebrew voices. Only the first 60 characters are used. 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. Custom (custom_<id>) voices, and Visual DNA (dna_<id>) voices backed by a cloned or designed voice, accept at most 5,000 characters (trimmed) — longer returns 400 TEXT_TOO_LONG
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_v4). Advisory — for catalog voices the synthesis model comes from the voice record, not from this field. See the callout below. eleven_v4_turbo is accepted as an alias for eleven_v4 with turbo: true
turbobooleanNoElevenLabs V4 Turbo. Must be the JSON boolean true (a string "true" is ignored). Only valid when the resolved voice is an ElevenLabs V4 voice — otherwise 400 TURBO_UNSUPPORTED. Billed at the model's turbo credit rate
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". Trimmed and truncated to the first 500 characters. Ignored when style_instructions_preset_id names a preset (or is none)
style_instructions_preset_idstringNoGoogle / Gemini voices only. A named voice-direction preset that the server expands into the directive the engine receives: warm, dramatic, whisper, excited, calm, cheerful, serious, storyteller, sad, intimate, british, commercial — plus custom (use style_instructions, or style_instructions_label when that is empty) and none (default delivery, drops any style_instructions). Any other value returns 400 INVALID_STYLE_PRESET with the valid list
style_instructions_labelstringNoGoogle / Gemini voices only. Display label stored with the generation (defaults to the preset's label or the directive). Truncated to 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.7–1.2 speed, Cartesia clamps to 0.6–1.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
session_idstringNoPut this generation into an existing text-to-speech session (e.g. the session_id from an earlier speech call) instead of a new one. Wins over project_id. Malformed → 400 SDK_SESSION_INVALID_ID; missing, or not a speech session → 404 SDK_SESSION_NOT_FOUND

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 and the TTS engine resolves it: a dna_<visualDnaId> value (a Visual DNA character voice) resolves to that character's voice (404, e.g. Character voice not found, when it cannot), and a custom_<id> that is not one of your non-deleted voices (or is a Fish Audio clone that is not ready yet) returns 404 Custom voice is not available. Anything else that is not a real catalog voice_id fails with 400 Voice "<value>" not found.

Unless noted, these engine-level errors arrive in the standard { "success": false, "error": ..., "code": ... } envelope.

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, VOICE_NOT_FOUND and INVALID_STYLE_PRESET 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 0–1 (default 0.75). Outside the range returns 400
stylenumberElevenLabsStyle exaggeration 0–1 (default 0.5). Outside the range returns 400
variancenumberDeepDubVoice variance 0–1. Outside the range returns 400
temponumberDeepDubTempo multiplier 0–2. 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 0–1, otherwise 400
voiceTitlestringDeepDubDisplay title for a custom/cloned voice
minimax_pitchnumberMiniMaxPitch -12–12 (default 0)
minimax_volnumberMiniMaxVolume 0–10 (default 1)
minimax_intensitynumberMiniMaxVoice intensity
minimax_timbrenumberMiniMaxVoice timbre

The Google / Gemini direction fields (style_instructions, style_instructions_preset_id, style_instructions_label) are documented in the main table above.

The fields in both tables (plus turbo) are an explicit allowlist: any other field in the request body — including the app-only fish_settings — 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_v4"
  }'

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_v4",
  "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 resolved identifier: what you sent, or the default eleven_v4 when you omitted it, sent an auto-select alias, or sent an identifier the catalog does not know. eleven_v4_turbo is echoed as eleven_v4
credits_chargednumberThe character-based pre-calculation (see Credits). 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. Pass this session_id back on later speech calls to keep them in one session

Completed Status

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

{
  "success": true,
  "generation_id": "68f2c1a9b4e5d6f7a8b9c0d1",
  "type": "speech",
  "state": "completed",
  "progress": 100,
  "session_id": "68f2c1a9b4e5d6f7a8b9c0d2",
  "project_id": "68f2c1a9b4e5d6f7a8b9c0d3",
  "result": {
    "urls": ["https://media.kolbo.ai/tts/.../speech.mp3"],
    "voice": "EXAVITQu4vr4xnSDxMaL",
    "requested_voice_id": "EXAVITQu4vr4xnSDxMaL",
    "duration": null,
    "speaking_speed": null,
    "language": "en-US",
    "selected_style": null,
    "style_instructions": null,
    "style_instructions_label": null,
    "style_instructions_preset_id": null,
    "voice_settings": { "similarity_boost": 0.75, "style": 0.5, "stability": 1 },
    "model": "eleven_v4",
    "created_at": "2026-07-20T14:20:00Z"
  },
  "credits_used": 6,
  "credits_breakdown": [
    { "model": "eleven_v4", "amount": 6 }
  ]
}

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 carries success, generation_id, type, state, progress and (when known) session_id / project_id, with no result at all. There is no partial or streaming read. credits_breakdown entries carry only model and amount; both credit fields are omitted when no deduction row is found.

result fieldNotes
voiceThe voice the provider actually spoke (resolved id), not the name you sent
requested_voice_idThe voice id the request resolved to before synthesis. Differs from voice when the provider substitutes (e.g. a Google voice whose locale prefix is discarded)
modelThe 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
model_namePresent only when the record stores a resolved display name
durationnull in practice on this path: the TTS record does not persist an output length, so measure the file yourself if you need it
speaking_speed, language, selected_styleThe delivery settings stored with the generation (selected_style falls back to the stored emotion), so you can reuse them
style_instructions, style_instructions_label, style_instructions_preset_idThe resolved Google / Gemini direction, or null
voice_settings{ similarity_boost, style, stability } — present only when the record stores voice settings
minimax{ pitch, vol, intensity, timbre } — present only for MiniMax generations that stored pitch or volume
accentControlThe DeepDub accent object, when one was sent
created_atCreation timestamp

Failure and cancellation

A failed synthesis is still an HTTP 200 with success: true — the failure is in state. error and failure.message carry the stored user-facing reason (provider names scrubbed). The other failure.* sub-fields (category, code, retryable, severity) are normally null on this endpoint, because the TTS failure record stores its cause under different keys. A job that never gets a provider response is failed by the 15-minute server deadline with a "timed out" 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 the model that actually runs (the voice's own model — see the callout above). Fish Audio models count in blocks of 10 characters instead of 100. turbo: true applies the model's turbo credit adjustment to that total.

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

Before any job is created the SDK requires a balance of at least 2 credits (403 INSUFFICIENT_CREDITS, with balance, required and pricing_url); the engine then checks the full estimate and returns 403 INSUFFICIENT_CREDITS if it is not covered. In a shared project the project owner's credits are checked.


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. Non-English prompts are translated to English server-side before generation (except on Seed-Audio, which receives your original text)
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 the model's default duration (falling back to 8s)
prompt_influencenumberNoElevenLabs: prompt guidance 0–1 (default 0.5). Higher = more literal
project_idstringNoRoute the output into a specific project. Omit for the default "API Generations" project
session_idstringNoPut this generation into an existing text-to-sound session (e.g. the session_id from an earlier sound call). Wins over project_id. Malformed → 400 SDK_SESSION_INVALID_ID; missing, or not a sound session → 404 SDK_SESSION_NOT_FOUND

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_loopbooleanElevenLabs / KieGenerate a seamlessly looping sound
sound_temponumberKieTempo control
sound_keystringKieMusical key / scale. Kie models do not receive duration
seed_voicestringFAL Seed-AudioVoice to use
seed_speednumberFAL Seed-AudioSpeed multiplier 0.5–2.0
seed_volumenumberFAL Seed-AudioVolume 0–1
seed_pitchnumberFAL Seed-AudioPitch shift in semitones
seed_reference_audio_urlsstring[]FAL Seed-AudioReference audio URLs — up to 3. Must be a JSON array; only the first 3 entries are forwarded (empty or non-string entries among them are then discarded) and the rest are silently dropped. URLs pointing at internal/private hosts fail the generation
seed_reference_image_urlstringFAL Seed-AudioOne reference image URL (same internal-host rule)
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 resolved identifier: what you sent, or the default elevenlabs-sound-effects-v1 when you omitted it, sent an auto-select alias, or sent an identifier the catalog does not know
credits_chargednumberThe pre-calculated estimate (see Sound credits). 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
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 }
  ]
}

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 carries success, generation_id, type, state, progress and (when known) session_id / project_id, with 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. result.model is the model that ran and result.created_at the creation timestamp.

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 stored error code when there is one; failure.category, retryable and severity are normally null on this endpoint. A job that never gets a provider response is failed by the 15-minute server deadline with a "timed out" message. 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);

Sound credits

A sound model charges a flat model.credit per generation unless it is billed per second. Per-second models charge ceil(model.credit x seconds): the estimate uses your duration (or the model's default duration, falling back to 8s), and the final charge uses the measured length of the output, rounded up. Credits are checked against your balance (or the project owner's in a shared project) before the job is created — 403 when not covered.

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, 1–100 characters after trimming. Blank or longer returns 400
audioFilefileYesThe voice sample. Missing file returns 400. Route cap: 25 MB (multer memory storage)
providerstringNoOne of deepdub, elevenlabs, minimax, zonos, cartesia, fish-audio. 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. You need owner, full or edit permission on it, otherwise 403

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
minimax2s—No provider-specific duration ceiling
fish-audio10s120sAdditional 20 MB file cap

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.

If the voice was created but its billing is still being finalized, the endpoint answers HTTP 202 with { "success": false, "error": "Your voice is being finalized..." }. That is not a failure: check GET /api/v1/voices again shortly before retrying, or you may clone twice.

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/clone30 / minute, shared with the SDK generation bucket
POST /v1/voices/import-elevenlabs30 / minute, shared with the SDK generation bucket
DELETE /v1/voices/:id120 / minute, shared with the SDK media bucket

Requests that end in an error status (4xx/5xx) do not count against a limit.

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 30 requests/minute per account (one bucket across the generation endpoints that use it; requests that end in an error status are not counted)
  • 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