
Client for ElevenLabs API enabling TTS (batch, HTTP-streamed, realtime/WebSocket), STT (Scribe batch and realtime), voice/model listing, session control, and rotating API-key providers.
A Kotlin-first Multiplatform SDK for the ElevenLabs API, with coroutine and Flow-based APIs for Android, iOS and JVM.
Community-maintained SDK. Not affiliated with or endorsed by ElevenLabs.
ElevenLabs KMP is available from Maven Central.
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}// Shared module's build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.yveskalume:elevenlabs-kmp:0.1.0")
}
}
}Development snapshots are also published to the Central Portal snapshots repository:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
maven {
url = uri("https://central.sonatype.com/repository/maven-snapshots/")
content {
includeModule("io.github.yveskalume", "elevenlabs-kmp")
}
}
mavenCentral()
}
}Then replace 0.1.0 with 0.1.0-SNAPSHOT in the dependency declaration.
Create a client in a trusted environment:
val elevenLabs = ElevenLabs {
apiKey(System.getenv("ELEVENLABS_API_KEY"))
}The configured key is used automatically by every service.
List available voices:
val page = elevenLabs.voices.list(
ListVoicesRequest(
pageSize = 25,
search = "narrator",
),
)Generate speech:
val audio = elevenLabs.textToSpeech.generate(
TextToSpeechRequest(
voiceId = "JBFqnCBsd6RMkjVDRZzb",
text = "Hello from Kotlin Multiplatform.",
modelId = "eleven_multilingual_v2",
),
)
saveAudio(audio.bytes)Call elevenLabs.close() when the client is no longer needed.
Choose the API based on how text becomes available:
| API | Use it when |
|---|---|
generate() |
You need the complete audio as one result. |
stream() |
The complete text is available, but audio should arrive incrementally over HTTP. |
realtime() |
Text arrives incrementally, such as tokens from an LLM. |
openRealtimeSession() |
You need explicit control over sending, flushing, or alignment events. |
Stream audio over HTTP:
elevenLabs.textToSpeech.stream(
TextToSpeechRequest(
voiceId = "JBFqnCBsd6RMkjVDRZzb",
text = "A longer passage to synthesize.",
),
).collect { chunk ->
audioPipeline.write(chunk.bytes)
}An AudioChunk is an arbitrary transport chunk and may not contain a complete codec frame.
Stream text and audio in realtime:
val llmText: Flow<String> = languageModel.responses()
elevenLabs.textToSpeech.realtime(
voiceId = "JBFqnCBsd6RMkjVDRZzb",
text = llmText,
options = RealtimeTtsOptions(
modelId = "eleven_flash_v2_5",
outputFormat = OutputFormat.Pcm_24000,
),
).collect { chunk ->
audioPipeline.write(chunk.bytes)
}For explicit control, open a session:
val session = elevenLabs.textToSpeech.openRealtimeSession(
voiceId = "JBFqnCBsd6RMkjVDRZzb",
options = RealtimeTtsOptions(syncAlignment = true),
)
coroutineScope {
launch {
session.events.collect { event ->
when (event) {
is RealtimeTtsEvent.Audio -> {
audioPipeline.write(event.bytes)
updateCaptions(event.normalizedAlignment)
}
RealtimeTtsEvent.Finished -> Unit
}
}
}
session.sendText("Hello ")
session.sendText("from realtime TTS.", flush = true)
session.finish()
}Use finish() to complete a session gracefully or close() to cancel it immediately.
Transcribe a complete audio or video file with Scribe:
val transcript = elevenLabs.speechToText.transcribe(
SpeechToTextRequest(
audio = recordedAudio,
fileName = "recording.m4a",
contentType = "audio/mp4",
modelId = "scribe_v2",
diarize = true,
),
)
println(transcript.text)For live microphone transcription, open a realtime session and send audio in the format declared by its options:
val session = elevenLabs.speechToText.openRealtimeSession(
options = RealtimeSttOptions(
audioFormat = RealtimeSttAudioFormat.Pcm16000,
commitStrategy = RealtimeSttCommitStrategy.VoiceActivityDetection,
includeTimestamps = true,
),
)
coroutineScope {
launch {
session.events.collect { event ->
when (event) {
is RealtimeSttEvent.PartialTranscript -> showLiveText(event.text)
is RealtimeSttEvent.CommittedTranscript -> saveText(event.text)
else -> Unit
}
}
}
microphone.pcm16MonoChunks(sampleRate = 16_000).collect { chunk ->
session.sendAudio(chunk)
}
session.commit()
}Close the session when microphone capture ends.
For rotating credentials, configure a provider instead of a static key:
val elevenLabs = ElevenLabs {
apiKey(
ApiKeyProvider {
credentialsStore.currentElevenLabsKey()
},
)
}Never embed an ElevenLabs API key in a production Android or iOS application. Use short-lived credentials issued by a trusted backend instead. See the authentication guide.
https://yveskalume.github.io/elevenlabs-kmp
The Android and iOS sample apps demonstrate text-to-speech and live microphone transcription. Development keys configured for these samples are embedded in the resulting app and must not be used for production builds.
For Android, add the following to the ignored local.properties file:
ELEVENLABS_API_KEY=your-development-keyFor iOS, create the ignored secrets configuration and replace its placeholder value:
cp iosApp/Configuration/Secrets.xcconfig.example \
iosApp/Configuration/Secrets.xcconfigA Kotlin-first Multiplatform SDK for the ElevenLabs API, with coroutine and Flow-based APIs for Android, iOS and JVM.
Community-maintained SDK. Not affiliated with or endorsed by ElevenLabs.
ElevenLabs KMP is available from Maven Central.
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}// Shared module's build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.yveskalume:elevenlabs-kmp:0.1.0")
}
}
}Development snapshots are also published to the Central Portal snapshots repository:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
maven {
url = uri("https://central.sonatype.com/repository/maven-snapshots/")
content {
includeModule("io.github.yveskalume", "elevenlabs-kmp")
}
}
mavenCentral()
}
}Then replace 0.1.0 with 0.1.0-SNAPSHOT in the dependency declaration.
Create a client in a trusted environment:
val elevenLabs = ElevenLabs {
apiKey(System.getenv("ELEVENLABS_API_KEY"))
}The configured key is used automatically by every service.
List available voices:
val page = elevenLabs.voices.list(
ListVoicesRequest(
pageSize = 25,
search = "narrator",
),
)Generate speech:
val audio = elevenLabs.textToSpeech.generate(
TextToSpeechRequest(
voiceId = "JBFqnCBsd6RMkjVDRZzb",
text = "Hello from Kotlin Multiplatform.",
modelId = "eleven_multilingual_v2",
),
)
saveAudio(audio.bytes)Call elevenLabs.close() when the client is no longer needed.
Choose the API based on how text becomes available:
| API | Use it when |
|---|---|
generate() |
You need the complete audio as one result. |
stream() |
The complete text is available, but audio should arrive incrementally over HTTP. |
realtime() |
Text arrives incrementally, such as tokens from an LLM. |
openRealtimeSession() |
You need explicit control over sending, flushing, or alignment events. |
Stream audio over HTTP:
elevenLabs.textToSpeech.stream(
TextToSpeechRequest(
voiceId = "JBFqnCBsd6RMkjVDRZzb",
text = "A longer passage to synthesize.",
),
).collect { chunk ->
audioPipeline.write(chunk.bytes)
}An AudioChunk is an arbitrary transport chunk and may not contain a complete codec frame.
Stream text and audio in realtime:
val llmText: Flow<String> = languageModel.responses()
elevenLabs.textToSpeech.realtime(
voiceId = "JBFqnCBsd6RMkjVDRZzb",
text = llmText,
options = RealtimeTtsOptions(
modelId = "eleven_flash_v2_5",
outputFormat = OutputFormat.Pcm_24000,
),
).collect { chunk ->
audioPipeline.write(chunk.bytes)
}For explicit control, open a session:
val session = elevenLabs.textToSpeech.openRealtimeSession(
voiceId = "JBFqnCBsd6RMkjVDRZzb",
options = RealtimeTtsOptions(syncAlignment = true),
)
coroutineScope {
launch {
session.events.collect { event ->
when (event) {
is RealtimeTtsEvent.Audio -> {
audioPipeline.write(event.bytes)
updateCaptions(event.normalizedAlignment)
}
RealtimeTtsEvent.Finished -> Unit
}
}
}
session.sendText("Hello ")
session.sendText("from realtime TTS.", flush = true)
session.finish()
}Use finish() to complete a session gracefully or close() to cancel it immediately.
Transcribe a complete audio or video file with Scribe:
val transcript = elevenLabs.speechToText.transcribe(
SpeechToTextRequest(
audio = recordedAudio,
fileName = "recording.m4a",
contentType = "audio/mp4",
modelId = "scribe_v2",
diarize = true,
),
)
println(transcript.text)For live microphone transcription, open a realtime session and send audio in the format declared by its options:
val session = elevenLabs.speechToText.openRealtimeSession(
options = RealtimeSttOptions(
audioFormat = RealtimeSttAudioFormat.Pcm16000,
commitStrategy = RealtimeSttCommitStrategy.VoiceActivityDetection,
includeTimestamps = true,
),
)
coroutineScope {
launch {
session.events.collect { event ->
when (event) {
is RealtimeSttEvent.PartialTranscript -> showLiveText(event.text)
is RealtimeSttEvent.CommittedTranscript -> saveText(event.text)
else -> Unit
}
}
}
microphone.pcm16MonoChunks(sampleRate = 16_000).collect { chunk ->
session.sendAudio(chunk)
}
session.commit()
}Close the session when microphone capture ends.
For rotating credentials, configure a provider instead of a static key:
val elevenLabs = ElevenLabs {
apiKey(
ApiKeyProvider {
credentialsStore.currentElevenLabsKey()
},
)
}Never embed an ElevenLabs API key in a production Android or iOS application. Use short-lived credentials issued by a trusted backend instead. See the authentication guide.
https://yveskalume.github.io/elevenlabs-kmp
The Android and iOS sample apps demonstrate text-to-speech and live microphone transcription. Development keys configured for these samples are embedded in the resulting app and must not be used for production builds.
For Android, add the following to the ignored local.properties file:
ELEVENLABS_API_KEY=your-development-keyFor iOS, create the ignored secrets configuration and replace its placeholder value:
cp iosApp/Configuration/Secrets.xcconfig.example \
iosApp/Configuration/Secrets.xcconfig