← Blog

Claude API Android Integration with Kotlin

2026-10-21 · 5 min read · SubToAPI Team

How to call Claude from a Kotlin Android app

Integrating Claude into an Android app with Kotlin comes down to three pieces: a networking client (OkHttp or Retrofit) that sends HTTPS requests to a Messages-style endpoint, a backend layer that holds your real API credentials so they never ship inside the APK, and Kotlin code that parses JSON responses (or SSE streams) into something your UI can render. You are not calling a special "Android SDK" — Claude's API is just HTTPS and JSON, so any Kotlin HTTP client works.

The part developers usually get wrong is key placement. Anthropic API keys are long-lived secrets. If you embed one directly in your Android app, it can be extracted from the APK in minutes with a decompiler, and anyone who pulls it out can run unlimited requests on your account. The fix is to never put your root key on-device: route requests through a backend, or use a layer like SubToAPI that issues per-app keys (sub_live_...) you can scope, rotate, and revoke without touching your main Claude access.

Architecture: direct backend vs a managed API layer

There are two common setups for an Android + Claude integration:

  1. Your own backend proxy — a small server (Node, Kotlin/Ktor, whatever) holds the real credentials, forwards requests to Claude, and returns the response to the app. You own the infrastructure, logging, and rate limiting.
  2. A managed API layer — a service like SubToAPI sits between your app and Claude. You create application-scoped keys in a dashboard, the app calls those keys over plain HTTPS, and you get streaming, usage metadata, and team seats without maintaining a proxy server yourself.

For a side project or an MVP, option 2 is faster: no server to deploy, no uptime to babysit. For a large team with existing infrastructure, option 1 might fit better. Either way, the Kotlin code in your Android app looks almost identical — it's just hitting a different base URL.

Setting up the Kotlin client

Add OkHttp and kotlinx-serialization (or Moshi/Gson, your choice) to build.gradle.kts:

dependencies {
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
}

Define request/response models:

@Serializable
data class Message(val role: String, val content: String)

@Serializable
data class ClaudeRequest(
    val model: String = "claude-3-5-sonnet-20241022",
    val max_tokens: Int = 1024,
    val messages: List<Message>
)

@Serializable
data class ClaudeResponse(
    val id: String,
    val content: List<ContentBlock>
)

@Serializable
data class ContentBlock(val type: String, val text: String? = null)

A simple suspend function to send a request:

suspend fun askClaude(prompt: String): String = withContext(Dispatchers.IO) {
    val client = OkHttpClient()
    val json = Json { ignoreUnknownKeys = true }

    val body = json.encodeToString(
        ClaudeRequest(messages = listOf(Message("user", prompt)))
    )

    val request = Request.Builder()
        .url("https://api.subtoapi.app/v1/messages")
        .addHeader("Authorization", "Bearer ${BuildConfig.SUBTOAPI_KEY}")
        .addHeader("Content-Type", "application/json")
        .post(body.toRequestBody("application/json".toMediaType()))
        .build()

    client.newCall(request).execute().use { response ->
        val parsed = json.decodeFromString<ClaudeResponse>(response.body!!.string())
        parsed.content.firstOrNull { it.type == "text" }?.text ?: ""
    }
}

Note BuildConfig.SUBTOAPI_KEY — even for an app-scoped key, pull it from a build config field populated at build time (or better, fetch a short-lived token from your own login flow) rather than hardcoding it in source. App-scoped keys limit blast radius, but they're still bearer tokens.

Full request/response shape for the endpoint is documented at /docs/messages; the /docs/quickstart page walks through getting a key and making your first call.

Streaming responses into a RecyclerView or Compose UI

Chat-style Android apps feel much better with token streaming instead of waiting for a full response. OkHttp handles Server-Sent Events with an EventSource listener:

val factory = EventSources.createFactory(client)
val listener = object : EventSourceListener() {
    override fun onEvent(eventSource: EventSource, id: String?, type: String?, data: String) {
        val chunk = json.decodeFromString<StreamChunk>(data)
        chunk.delta?.text?.let { text ->
            // append to a StateFlow<String> your Compose UI collects
            viewModelScope.launch { _responseText.emit(_responseText.value + text) }
        }
    }
}

val streamRequest = Request.Builder()
    .url("https://api.subtoapi.app/v1/messages")
    .addHeader("Authorization", "Bearer ${BuildConfig.SUBTOAPI_KEY}")
    .post(body.toRequestBody("application/json".toMediaType()))
    .build()

factory.newEventSource(streamRequest, listener)

Set "stream": true in your request body to trigger this mode. Details on the event types (content_block_delta, message_stop, etc.) are in /docs/streaming. In Compose, collect the StateFlow<String> in a LaunchedEffect and render it directly into a Text composable — the UI updates token by token with no extra plumbing.

Handling tool use and structured actions

If your Android app needs Claude to trigger actions — look up a user's order, fetch weather, run a calculation — define tools in the request and parse tool_use blocks in the response:

val toolRequest = ClaudeRequest(
    messages = listOf(Message("user", "What's my order status for #4471?")),
)
// tools array added to the raw JSON payload per /docs/tools

When Claude returns a tool_use content block, your Kotlin code executes the matching local function (hit your own order API, query Room, whatever), then sends the result back as a tool_result message in the next turn. This round-trip pattern is the same regardless of platform — see /docs/tools for the exact message shapes.

Error handling and offline behavior

Mobile networks drop. Wrap calls with retry-with-backoff for 429/529 responses, and cache the last successful response locally (Room or DataStore) so the UI has something to show if the device goes offline mid-conversation. Always check the HTTP status code before parsing the body — a 401 almost always means a bad or revoked key, not a malformed request.

Getting started

If you'd rather not stand up and maintain your own proxy server, sign up for a free trial and generate an application key in minutes. Pricing starts at Solo (€9) for individual apps, with Team (€19/seat) and Scale (€49/seat) plans for bigger builds — details on /pricing.

Questions

Do I need a backend server to use Claude in an Android app? Not strictly — you need something that holds your API credentials instead of the device. That can be your own server or a managed layer like SubToAPI that issues app-scoped keys over HTTPS.

Can I stream Claude's response in real time on Android? Yes. Use OkHttp's EventSource to consume server-sent events and append incoming text chunks to a StateFlow that your Compose UI or RecyclerView adapter observes.

Is it safe to store an Anthropic key in BuildConfig? It's safer than hardcoding it in source, but any string compiled into the APK can be extracted. Use scoped, revocable keys and avoid embedding your root account credentials in any client build.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →