Skip to content
Moku
@moku-labs/ai pages

elevenlabs

Provider. Voiceover, sound effects and music over the ElevenLabs HTTP API, with a bundled price table.

On this page

What it does

Provider. elevenlabs owns everything ElevenLabs-specific. It registers three handlers with registry in onInit: ("voiceover", "elevenlabs"), ("sfx", "elevenlabs") and ("music", "elevenlabs"). It talks to the API with a thin fetch client, no SDK. Each handler has estimate and execute. The runner and the task facades reach them through the registry. You never call a handler directly.

Its own public API is one method, app.elevenlabs.info().

Setup

One env var. Exported in the shell or in .env.local.

VariableNeeded bySent as
ELEVENLABS_API_KEYexecute onlythe xi-api-key header

info() and every estimate() work without the key. When the variable is unset, execute throws before any network call:

[ai] ELEVENLABS_API_KEY is not set.
  Export it or set config.apiKeyEnv to the variable that holds your key.

The key is read through ctx.env at request time. It is never stored and never logged.

Tasks it serves

TaskEndpointDefault provider of the task
voiceoverPOST /v1/text-to-speech/{voiceId}yes: elevenlabs registers first
sfxPOST /v1/sound-generationyes
musicPOST /v1/musicno: the default is fal, name provider: elevenlabs

voiceover

text, voice (an ElevenLabs voice id), optional language, model, format and params. format is mp3 (default), wav or ogg. params is spread into the JSON body last, so its keys win.

hello.moku.yaml
  - id: greeting
    task: voiceover
    provider: elevenlabs
    input:
      text: "Hello, world!"
      voice: "21m00Tcm4TlvDq8ikWAM"
      format: mp3

sfx

The output is always mp3. Every rule is checked before any HTTP call. A refused request is a terminal 400: the runner marks the item failed and never retries it.

FieldRule
modelMust be "eleven_text_to_sound_v2".
promptAt most 450 characters.
durationMsOptional. 500 to 30000. Omitted: the model picks the length.
promptInfluenceOptional. 0 to 1.
loopOptional boolean.
params.output_formatOptional. Must start with mp3_. Default "mp3_44100_128". The only params key the handler reads.
hello.moku.yaml
  - id: sword-hit
    task: sfx
    input:
      prompt: "sword hit, metallic"
      model: eleven_text_to_sound_v2
      durationMs: 800

music

Paid ElevenLabs plans only. On a free plan the call fails with a 4xx. The endpoint answers when the whole track is ready, so it has its own timeout, musicTimeoutMs.

FieldRule
model"music_v1", "music_v2" or "music_v2_5". The fal alias "elevenlabs-music-v2.5" is refused.
lengthMsWhole number, 3000 to 600000.
promptSent when there are no chunks. Must not be empty.
chunksA composition plan. music_v2 and music_v2_5 only. The chunk durations must add up to lengthMs.
chunks[].durationMsWhole number, 3000 to 120000.
chunks[].textAt most 30 lines of at most 200 characters.
seedOnly with chunks.
params.force_instrumentalOptional boolean, only with a prompt. Default true.
params.output_formatOptional. Must start with mp3_.
any other params keyRefused.
hello.moku.yaml
  - id: s01.score
    task: music
    provider: elevenlabs
    input: { model: music_v2_5, prompt: "tense synth pulse", lengthMs: 30000 }
  - id: s01.theme
    task: music
    provider: elevenlabs
    input:
      model: music_v2_5
      prompt: "main theme"
      lengthMs: 43000
      seed: 7
      chunks:
        - { text: "[Intro]", durationMs: 13000, styles: [synthwave, dark, slow build] }
        - { text: "[Drop]", durationMs: 30000, styles: [driving bass], avoid: [vocals] }

Models and prices

The plugin ships a price table. priceOverrides merges over it, and an override wins. The effective table is computed once at first use and cached. The bundled numbers are estimates: set your plan's real price.

Voice models, USD per character:

ModelUSD / character
eleven_multilingual_v20.0003
eleven_turbo_v2_50.00015
eleven_flash_v2_50.00006
eleven_monolingual_v10.0003

A voice model missing from the table estimates at $0.

Sound effects, keyed sfx:<model>#second and sfx:<model>#auto:

KeyUSD
sfx:eleven_text_to_sound_v2#second0.002 per started second
sfx:eleven_text_to_sound_v2#auto0.01 per generation, when the request has no durationMs

Music, keyed music:<model>, per started minute:

KeyUSD
music:music_v10.15
music:music_v20.15
music:music_v2_50.15

A missing sfx or music price is a terminal error, not $0. A paid job never runs at an unknown price:

[ai] No price for ElevenLabs sfx model "eleven_text_to_sound_v2".
  Add it to elevenlabs.priceOverrides.

Cost math: voiceover is text.length × price. sfx is ceil(durationMs / 1000) × #second, or #auto without a duration. music is every started minute × music:<model>. estimate and execute use the same formula, so costUsd equals the estimate.

moku.config.ts
import { createApp } from "@moku-labs/ai";

const app = createApp({
  pluginConfigs: {
    elevenlabs: {
      defaultModel: "eleven_turbo_v2_5",
      priceOverrides: {
        eleven_turbo_v2_5: 0.00012,
        "sfx:eleven_text_to_sound_v2#second": 0.0025
      }
    }
  }
});

console.log(app.elevenlabs.info().models); // includes "eleven_turbo_v2_5"
  1. The real per-second price of your plan.

Config

pluginConfigs.elevenlabs.

OptionTypeDefaultDescription
apiKeyEnvstring"ELEVENLABS_API_KEY"Name of the env var with the API key. Read through ctx.env at request time.
baseUrlstring"https://api.elevenlabs.io"API base URL.
defaultModelstring"eleven_multilingual_v2"Voice model when a request names none.
timeoutMsnumber60_000Timeout of one voiceover or sfx request, ms. Merged with the caller's signal.
musicTimeoutMsnumber600_000Timeout of one music request, ms.
priceOverridesRecord<string, number>{}Prices merged over the bundled table. Voice models by id, USD per character. sfx:<model>#second, sfx:<model>#auto, music:<model> as above.

API

Mounted at app.elevenlabs.

MethodSignatureDescription
info() => { provider: "elevenlabs"; configured: boolean; models: string[] }configured is whether the key env var is set, checked with ctx.env.has. models is the voice model ids of the effective price table, overrides included. sfx: and music: rows are left out. Never throws.
status.ts
import { app } from "./app";

const { configured, models } = app.elevenlabs.info();
if (!configured) throw new Error("Export ELEVENLABS_API_KEY first.");
console.log(models); // ["eleven_multilingual_v2", "eleven_turbo_v2_5", ...]

Types ship under the Elevenlabs namespace: Elevenlabs.Config, Elevenlabs.ElevenlabsApi.

Errors

The client maps every failure to one of three classes. The runner reads them by shape (status, kind, retryAfterMs), never by class.

FailureClassFieldsRunner
HTTP 5xxRetryableProviderErrorstatushttp-5xx, retried
HTTP 429RetryableProviderErrorstatus: 429, retryAfterMs from Retry-Afterhttp-429, retried after the hint
TimeoutRetryableProviderErrorkind: "timeout"timeout, retried
Network failureRetryableProviderErrorkind: "network"network, retried
Other HTTP 4xx, a broken request ruleTerminalProviderErrorstatushttp-4xx, item failed
detail.status is content_policy_violation, bad_prompt or bad_composition_planFlaggedProviderErrorkind: "content-policy"content-policy, item flagged, never re-queued

A content-policy detail.status in the body wins over the numeric status. An abort through opts.signal is rethrown unchanged. Messages carry status codes and error classes only, never request text or response bodies.

The classes are values under ElevenlabsErrors:

handle.ts
import { ElevenlabsErrors } from "@moku-labs/ai";
import { app } from "./app";

try {
  await app.voiceover.generate({ text: "Hi", voice: "21m00Tcm4TlvDq8ikWAM" });
} catch (error) {
  if (error instanceof ElevenlabsErrors.RetryableProviderError) {
    console.warn("transient", error.status ?? error.kind, error.retryAfterMs);
  } else if (error instanceof ElevenlabsErrors.FlaggedProviderError) {
    console.error("content policy, do not re-queue");
  } else {
    throw error;
  }
}
  • A music timeout is retried, and ElevenLabs may bill the first attempt. Raise musicTimeoutMs for long tracks.
  • A refused music prompt is flagged. bad_prompt comes back for artist names, band names and known lyrics. The API's suggested rewrite is not read into the error.
  • One key, no pool. The runner schedules the work on the lane "{task}/elevenlabs/default". Throttle it with limits.

Events

Emits

None.

Listens to

None. Diagnostics go through ctx.log: elevenlabs:<task>:done on success, and elevenlabs:<task>:failed with the error class and status before a rethrow. No message text.

Depends on