Skip to content
KeylayerDocs
Docs menu
PricingGet started

Use ElevenLabs with a scoped key and a spend cap

Set the ElevenLabs SDK's base URL to https://relay.keylayer.dev and its API key to a Keylayer key to get spend caps priced at $0.04 to $0.08 per 1,000 characters of speech, $0.22 an hour of transcribed audio, $0.12 a minute of sound effects and $0.15 a minute of music. Keylayer sends each call on to api.elevenlabs.io with your real key in the xi-api-key header; caps are estimated rather than hard, and a request can run for up to 900 seconds.

Set up

Add your ElevenLabs key as a credential and mint a key for it, as in the quickstart. Use a restricted ElevenLabs key with only the scopes for the endpoints you call (such as Text to Speech or Speech to Text) and a credit quota. Put the Keylayer key in an environment variable:

Shell
export KEYLAYER_KEY=kl_k_YOUR_KEY
Python (elevenlabs)
import os

from elevenlabs.client import ElevenLabs

client = ElevenLabs(
    base_url="https://relay.keylayer.dev",
    api_key=os.environ["KEYLAYER_KEY"],
)

audio = client.text_to_speech.convert(
    voice_id="JBFqnCBsd6RMkjVDRZzb",
    model_id="eleven_multilingual_v2",
    text="The first move is what sets everything in motion.",
    output_format="mp3_44100_128",
)

with open("speech.mp3", "wb") as file:
    for chunk in audio:
        file.write(chunk)
Node.js (@elevenlabs/elevenlabs-js)
import { writeFile } from "node:fs/promises";

import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";

const client = new ElevenLabsClient({
  baseUrl: "https://relay.keylayer.dev",
  apiKey: process.env.KEYLAYER_KEY,
});

const audio = await client.textToSpeech.convert("JBFqnCBsd6RMkjVDRZzb", {
  modelId: "eleven_multilingual_v2",
  text: "The first move is what sets everything in motion.",
  outputFormat: "mp3_44100_128",
});

await writeFile("speech.mp3", Buffer.from(await new Response(audio).arrayBuffer()));
curl
curl "https://relay.keylayer.dev/v1/text-to-speech/JBFqnCBsd6RMkjVDRZzb?output_format=mp3_44100_128" \
  -H "xi-api-key: $KEYLAYER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "The first move is what sets everything in motion.",
    "model_id": "eleven_multilingual_v2"
  }' \
  --output speech.mp3

Pass the key explicitly, as above. Both SDKs otherwise fall back to ELEVENLABS_API_KEY. The relay reads the Keylayer key from the same xi-api-key header ElevenLabs uses, so curl and other HTTP clients only change the host.

Spend caps on ElevenLabs

ElevenLabs keys take estimated spend caps only. Keylayer prices text-to-speech (POST /v1/text-to-speech/{voice_id}) and text-to-dialogue (POST /v1/text-to-dialogue), including their /stream, /with-timestamps and /stream/with-timestamps forms, per character at ElevenLabs' API prices, checked on 2026-10-03:

Model Per 1,000 characters
eleven_v4_turbo, eleven_flash_v2_5, eleven_flash_v2, eleven_turbo_v2_5, eleven_turbo_v2 $0.04
eleven_v4, eleven_multilingual_v2, eleven_v3 $0.08

A request without model_id is priced as eleven_multilingual_v2 for speech and eleven_v3 for dialogue.

Before forwarding, Keylayer reserves one character's price for every byte of the request body, so a 96-byte request on eleven_multilingual_v2 reserves $0.00768. After the response, it charges the characters of your text at the model's rate, or ElevenLabs' character-cost response header at $0.08 per 1,000, whichever is higher. An answer in the 400s or 500s is free, except a 504 or 524 timeout, which is charged the reservation, as is a request cancelled before the audio ends.

Keylayer can't price some speech requests up front: those with pronunciation dictionaries, apply_text_normalization set to on or apply_language_text_normalization set to true. It lets them through at a fallback estimate of $0.02 per request plus $0.02 per 1,000 bytes of body, and charges them from the character-cost header at $0.08 per 1,000 characters when ElevenLabs sends one, or the fallback when it doesn't. A model_id missing from the table above gets 403 unpriced_operation, unless the key has escape hatches, which let it through on the same terms. Every other POST, PUT or PATCH call, such as voice cloning, dubbing and outbound calls, gets 403 unpriced_operation, except cancelling a batch call, a knowledge base crawl or a test invocation and creating a music composition plan, which count $0.000001 each. So does a body Keylayer can't read, such as a speech request with a duplicate key.

Warning: For the calls Keylayer can't price, use a separate key with no spend cap on it or on a key above it, and limit it with request caps instead. Escape hatches let them through a spend cap at the fallback, which grows with the upload: a 1 MB upload to voice cloning reserves about $20 and is charged in full, even when ElevenLabs rejects it.

GET and DELETE calls, such as listing voices, reserve $0.000001 each.

Speech to text

Keylayer prices speech-to-text (POST /v1/speech-to-text) on scribe_v2 and scribe_v2_medical at $0.22 an hour of audio, plus each add-on you set, at ElevenLabs' API prices checked on 2026-10-23:

Add-on Added per hour
keyterms, billed for at least 20 seconds $0.05
entity_detection or entity_redaction $0.066
transcript_edit, billed for at least 10 seconds $0.07
detect_speaker_roles $0.022

keyterms and transcript_edit count whenever they're in the form, entity_detection and entity_redaction unless they're null (the Python SDK sends null when you leave them out), and detect_speaker_roles unless it's false.

Python (elevenlabs)
with open("call.mp3", "rb") as audio:
    transcript = client.speech_to_text.convert(file=audio, model_id="scribe_v2")

print(transcript.text)

Before forwarding, Keylayer reserves the upload as 16 kbit/s audio, at least 10 seconds, or an hour when the audio comes from source_url or cloud_storage_url, and five channels with use_multi_channel. A 1 MB upload on scribe_v2 reserves $0.031. After the response, it charges the audio_duration_secs ElevenLabs reports, by the second. With use_multi_channel, the default separate reply is charged for every channel at the longest channel's length. A reply with multichannel_output_style set to combined doesn't say how many channels ElevenLabs billed, so it's charged for five, as is a mono file sent with use_multi_channel, which gets the same reply. Uploads over 16 MiB get 413, so send longer recordings as source_url. A response without a duration, which ElevenLabs sends for audio that's silent throughout, is charged the reservation plus 10 hours at the request's rate: $2.20 on scribe_v2, or up to $3.62 with add-ons. Skip silent audio before sending it. A 504 or 524 timeout, a request still running after 900 seconds and one your client hangs up on that ElevenLabs hasn't finished 5 seconds later are charged the same. A file of several hours can keep ElevenLabs past 900 seconds, so send long files with webhook set to true on a key with escape hatches: ElevenLabs answers at once, and Keylayer charges the reservation alone. Any other answer in the 400s or 500s is free.

These get 403 unpriced_operation: another model, such as scribe_v1; a field ElevenLabs doesn't document; a query parameter other than enable_logging and token; and webhook set to anything but false, as ElevenLabs bills a webhook transcript after the response. Escape hatches let them through and charge the reservation, at Scribe v2 Realtime's $0.39 an hour for another model. A form Keylayer can't read gets the same 403 even with escape hatches: one that isn't multipart/form-data, sends a field twice (other than keyterms and the entity lists), or has more than 1,100 parts.

Sound effects, voice changer and voice isolator

Keylayer prices sound effects (POST /v1/sound-generation) on eleven_text_to_sound_v2 and eleven_text_to_sound_v3 at $0.12 a minute, ElevenLabs' API price checked on 2026-10-23. A request with duration_seconds from 0.5 to 30 is charged the length asked for, rounded up to the next tenth of a second, so a 2.5-second sound costs $0.005. Without it, ElevenLabs picks the length, and Keylayer charges the longest, 30 seconds or $0.06, so set duration_seconds whenever you know the length.

Python (elevenlabs)
sound = client.text_to_sound_effects.convert(text="A door creaks", duration_seconds=2.5)

with open("door.mp3", "wb") as file:
    for chunk in sound:
        file.write(chunk)

The reply is audio with no duration in it, so Keylayer charges that price once ElevenLabs answers, and nothing for an answer in the 400s or 500s other than a 504 or 524. Another model, a field ElevenLabs doesn't document or a query parameter other than output_format and enable_logging gets 403 unpriced_operation; escape hatches let it through at $0.06. A body that isn't one JSON object, or that sends a field twice, gets the same 403 even with escape hatches.

The voice changer (POST /v1/speech-to-speech/{voice_id} and /v1/voice-changer/{voice_id}) and the voice isolator (POST /v1/audio-isolation), each with /stream, get 403 unpriced_operation. Escape hatches let them through at $0.12 a minute of the upload read as 16 kbit/s audio, at least 10 seconds and at most the longest input ElevenLabs takes: 5 minutes, $0.60, for the voice changer and an hour, $7.20, for the isolator. That reservation is charged in full, even when ElevenLabs rejects the call, and audio encoded below 16 kbit/s, such as speech in Opus at 6 kbit/s, is charged for less time than it runs.

Music

Keylayer prices music (POST /v1/music, /v1/music/stream, /v1/music/detailed and /v1/music/detailed/stream) on music_v1, music_v2 and music_v2_5 at $0.15 a minute, ElevenLabs' API price checked on 2026-10-23, counted in the whole credits ElevenLabs bills music in, 1,590 a minute. A request with music_length_ms from 3,000 to 600,000 is charged that length rounded up to a whole credit, so a 10-second song costs $0.025 and a 3-second one $0.007548. A composition_plan is charged the sum of its sections and chunks, each the longest of its duration_ms and any range of another song it reuses.

A call without music_length_ms or a composition_plan reserves the longest song, 10 minutes or $1.50, and on every route but /v1/music/detailed it is charged that $1.50 whatever length ElevenLabs makes. To be charged the length you get, set music_length_ms, or call /v1/music/detailed: its reply carries the composition plan the song was made from, and Keylayer charges that plan's length.

Python (elevenlabs)
song = client.music.compose(prompt="Lo-fi beat", music_length_ms=10000)

with open("song.mp3", "wb") as file:
    for chunk in song:
        file.write(chunk)

Keylayer charges a song once ElevenLabs answers, and nothing for an answer in the 400s or 500s other than a 504 or 524. Another model, a field Keylayer can't price, such as lyrics_text, generation_mode or finetune_strength, or a query parameter other than output_format and enable_logging gets 403 unpriced_operation; escape hatches let it through at $1.50. Creating a composition plan (POST /v1/music/plan) costs no ElevenLabs credits, and Keylayer counts it at $0.000001. Uploading a song, stem separation and video-to-music get 403 unpriced_operation.

Blocked routes

Keylayer refuses these routes with 403 path_not_allowed, so a key can't mint ElevenLabs keys or change your account:

  • API keys and service accounts: /v1/api-keys/** and /v1/service-accounts/**
  • Your user and subscription: /v1/user/**
  • Your workspace: /v1/workspace/** and /v1/workspaces/**
  • Single-use tokens: /v1/single-use-token/**
  • Conversational AI signed URLs, conversation tokens, secrets and settings under /v1/convai/

Escape hatches lift the block for one key; see admin routes and escape hatches.

Errors

A Keylayer refusal has a keylayer-error header and an ElevenLabs-shaped body:

403 path_not_allowed
{"detail": {"type": "authorization_error", "code": "path_not_allowed", "message": "Keylayer path_not_allowed: this key does not allow this path.", "status": "path_not_allowed"}}

The Python SDK raises ApiError with status_code and headers. The Node.js SDK throws ElevenLabsError with statusCode, body and rawResponse.headers.

Python (elevenlabs)
import os

from elevenlabs.client import ElevenLabs
from elevenlabs.core.api_error import ApiError

client = ElevenLabs(
    base_url="https://relay.keylayer.dev",
    api_key=os.environ["KEYLAYER_KEY"],
)

try:
    audio = b"".join(
        client.text_to_speech.convert(
            voice_id="JBFqnCBsd6RMkjVDRZzb",
            model_id="eleven_multilingual_v2",
            text="The first move is what sets everything in motion.",
        )
    )
except ApiError as err:
    code = (err.headers or {}).get("keylayer-error")
    if code is None:
        raise
    print(f"Keylayer refused the request: {err.status_code} {code}")

The error reference lists every code.

What doesn't work

  • WebSocket endpoints, such as text-to-speech stream-input, realtime speech-to-text and Conversational AI sessions. The relay doesn't pass the Upgrade header on, so a WebSocket handshake can't complete.
  • The regional hosts in the SDKs' environment lists: api.us.elevenlabs.io and the EU, India and Singapore residency hosts. Keylayer sends every request to api.elevenlabs.io.
  • Hard spend caps.

Next steps

Updated

Create your first scoped key

Add a provider key once, then mint Keylayer keys with their own limits for each app, agent or teammate.

Get started