How the Ktor HTTP Client is Configured in the ElevenLabs SDK for API Calls

The ElevenLabs SDK configures a multiplatform HttpClient using Ktor's expect/actual pattern, with platform-specific engines (Darwin for iOS, OkHttp for Android) that share logging, JSON serialization, and default request settings across all API calls.

The kkoshin/muse repository implements a Kotlin Multiplatform SDK for the ElevenLabs API, where all network operations route through a centrally configured Ktor HTTP client. This architecture ensures consistent request handling, response parsing, and authentication regardless of whether the code runs on iOS or Android.

Multiplatform Client Architecture

At the core of the SDK, KtorClient.kt in the common source set declares an expect property that each platform must fulfill:

// elevenlabs/src/commonMain/kotlin/io/github/kkoshin/elevenlabs/KtorClient.kt
expect val ktorClient: HttpClient

This ktorClient serves as the single transport layer consumed by ElevenLabsClient for all API interactions. The expect/actual mechanism allows the SDK to define a unified interface while delegating engine-specific implementations to platform source sets.

iOS Configuration with the Darwin Engine

The iOS implementation resides in KtorClient.ios.kt and configures a native Darwin engine with comprehensive logging and serialization plugins:

Network Engine and Logging

The client uses the Darwin engine and installs the Logging plugin at LogLevel.ALL, routing messages to NSLog with a custom prefix:

// elevenlabs/src/iosMain/kotlin/io/github/kkoshin/elevenlabs/KtorClient.ios.kt
actual val ktorClient = HttpClient(Darwin) {
    install(Logging) {
        level = LogLevel.ALL
        logger = object : Logger {
            override fun log(message: String) {
                NSLog("Network/Ktor: $message")
            }
        }
    }
}

Serialization and Default Requests

Both platforms share identical JSON and request configurations. The iOS client installs ContentNegotiation with a kotlinx.serialization.json.Json instance configured with ignoreUnknownKeys = true and explicitNulls = false. The defaultRequest block sets Content-Type: application/json and prefixes all URLs with BASE_URL = "https://api.elevenlabs.io/v1/":

install(ContentNegotiation) {
    json(Json {
        ignoreUnknownKeys = true
        explicitNulls = false
    })
}

install(Resources)

defaultRequest {
    header(HttpHeaders.ContentType, ContentType.Application.Json)
    url(BASE_URL)
}

Android Configuration with the OkHttp Engine

The Android implementation in KtorClient.android.kt mirrors the iOS configuration but substitutes the OkHttp engine and routes logs through Android's native Log.d:

// elevenlabs/src/androidMain/kotlin/io/github/kkoshin/elevenlabs/KtorClient.android.kt
actual val ktorClient = HttpClient(OkHttp) {
    install(Logging) {
        level = LogLevel.ALL
        logger = object : Logger {
            override fun log(message: String) {
                Log.d("Network", message)
            }
        }
    }
    
    // ContentNegotiation, Resources, and defaultRequest 
    // identical to iOS implementation
}

This parity ensures that JSON serialization settings, base URL constants, and resource routing remain consistent across platforms, while each utilizes its native transport mechanism for optimal performance.

Consuming the Client in ElevenLabsClient

The ElevenLabsClient class consumes this pre-configured ktorClient and automatically injects authentication headers. Every request receives the xi-api-key header via a custom request pipeline:

// elevenlabs/src/commonMain/kotlin/io/github/kkoshin/elevenlabs/ElevenLabsClient.kt
class ElevenLabsClient(private val apiKey: String) {
    // Uses the shared ktorClient with automatic auth
}

Performing API Calls

The configured client supports type-safe resource routes enabled by the Resources plugin. For example, listing available voices uses the generated Voices resource class:

suspend fun listVoices(): Result<List<Voice>> {
    return elevenLabsClient.get<Voices, List<Voice>>(Voices())
}

For text-to-speech synthesis, the client serializes the request body using the shared JSON configuration:

suspend fun synthesize(text: String, voiceId: String): Result<TtsResponse> {
    val request = TextToSpeechRequest(text = text, voice = voiceId)
    return elevenLabsClient.post<TextToSpeechRequest, TtsResponse, TextToSpeech>(
        TextToSpeech(voiceId), 
        request
    )
}

Multipart uploads, such as voice verification, leverage the same client instance with platform-specific engine capabilities:

suspend fun uploadVerification(filePath: String): Result<VerificationResponse> {
    return elevenLabsClient.postForm<VerificationResponse>(Verification()) {
        append("audio_file", File(filePath).readBytes(), Headers.build {
            append(HttpHeaders.ContentType, "audio/wav")
        })
    }
}

Summary

  • The SDK declares an expect val ktorClient: HttpClient in the common source, implemented separately for iOS and Android.
  • iOS uses the Darwin engine with NSLog logging, while Android uses OkHttp with Android's Log.d.
  • Both platforms share identical configurations: BASE_URL = "https://api.elevenlabs.io/v1/", JSON serialization with ignoreUnknownKeys = true, and automatic Content-Type: application/json headers.
  • The Resources plugin enables type-safe API routes, and the Logging plugin captures all network traffic at LogLevel.ALL.
  • ElevenLabsClient automatically adds the xi-api-key header to every request made through the shared client.

Frequently Asked Questions

What HTTP engine does the ElevenLabs SDK use on iOS?

According to the source code in KtorClient.ios.kt, the iOS implementation uses the Darwin engine, which provides native URLSession-based networking on Apple platforms. This engine is preferred for iOS as it respects system proxy settings and TLS configurations.

How is the ElevenLabs API key added to requests?

The ElevenLabsClient class injects the API key via a headers block that appends xi-api-key to every outgoing request. This happens automatically when using the client wrapper, ensuring developers don't need to manually configure authentication headers for each API call.

What JSON serialization settings are configured in the Ktor client?

Both platforms configure a kotlinx.serialization.json.Json instance with ignoreUnknownKeys = true (to prevent crashes when the API adds new fields) and explicitNulls = false (to omit null values from requests). These settings are applied through the ContentNegotiation plugin in the client configuration.

Why does the SDK use the expect/actual pattern for the HTTP client?

The expect/actual pattern allows the SDK to maintain a single interface (ktorClient) in the common code while using platform-optimal engines—OkHttp for Android and Darwin for iOS. This approach maximizes compatibility with platform-specific networking features while keeping the API surface consistent across targets.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →