Kotlin's concise syntax, null safety, and coroutine support make it a strong choice for automation tools, Android apps, and backend services. When these applications encounter CAPTCHAs, CaptchaAI's HTTP API integrates seamlessly through OkHttp or Ktor client.
This guide covers reCAPTCHA v2/v3, Cloudflare Turnstile, and image CAPTCHA solving with production-ready Kotlin code using both coroutines and blocking approaches.
Why Kotlin for CAPTCHA Automation
- Coroutines — structured concurrency for parallel CAPTCHA solving without callback hell
- Null safety — compiler prevents NPE crashes in API response handling
- JVM ecosystem — access to all Java libraries (OkHttp, Apache HttpClient)
- Multiplatform — Ktor client works on JVM, Android, iOS, and native targets
- Data classes — clean API response modeling with minimal boilerplate
Prerequisites
Gradle (Kotlin DSL)
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
// Alternative: Ktor client
implementation("io.ktor:ktor-client-core:2.3.12")
implementation("io.ktor:ktor-client-cio:2.3.12")
implementation("io.ktor:ktor-client-content-negotiation:2.3.12")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.12")
}
Maven
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>1.8.1</version>
</dependency>
API Data Models
import kotlinx.serialization.Serializable
@Serializable
data class ApiResponse(
val status: Int,
val request: String
)
sealed class CaptchaTask {
data class RecaptchaV2(val sitekey: String, val pageUrl: String) : CaptchaTask()
data class RecaptchaV3(
val sitekey: String,
val pageUrl: String,
val action: String = "verify",
val minScore: Double = 0.7
) : CaptchaTask()
data class Turnstile(val sitekey: String, val pageUrl: String) : CaptchaTask()
data class ImageBase64(val body: String) : CaptchaTask()
}
class CaptchaException(message: String) : Exception(message)
Method 1: OkHttp (Blocking)
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import kotlinx.serialization.json.Json
class CaptchaSolver(private val apiKey: String) {
private val client = OkHttpClient.Builder()
.connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.build()
private val json = Json { ignoreUnknownKeys = true }
private val baseUrl = "https://ocr.captchaai.com"
fun solve(task: CaptchaTask): String {
val taskId = submit(task)
return poll(taskId)
}
private fun submit(task: CaptchaTask): String {
val formBody = FormBody.Builder()
.add("key", apiKey)
.add("json", "1")
when (task) {
is CaptchaTask.RecaptchaV2 -> formBody
.add("method", "userrecaptcha")
.add("googlekey", task.sitekey)
.add("pageurl", task.pageUrl)
is CaptchaTask.RecaptchaV3 -> formBody
.add("method", "userrecaptcha")
.add("googlekey", task.sitekey)
.add("pageurl", task.pageUrl)
.add("version", "v3")
.add("action", task.action)
.add("min_score", task.minScore.toString())
is CaptchaTask.Turnstile -> formBody
.add("method", "turnstile")
.add("key", task.sitekey)
.add("pageurl", task.pageUrl)
is CaptchaTask.ImageBase64 -> formBody
.add("method", "base64")
.add("body", task.body)
}
val request = Request.Builder()
.url("$baseUrl/in.php")
.post(formBody.build())
.build()
val response = client.newCall(request).execute()
val body = response.body?.string() ?: throw CaptchaException("Empty response")
val parsed = json.decodeFromString<ApiResponse>(body)
if (parsed.status != 1) throw CaptchaException("Submit failed: ${parsed.request}")
return parsed.request
}
private fun poll(
taskId: String,
maxWaitMs: Long = 300_000,
intervalMs: Long = 5_000
): String {
val deadline = System.currentTimeMillis() + maxWaitMs
while (System.currentTimeMillis() < deadline) {
Thread.sleep(intervalMs)
val url = HttpUrl.Builder()
.scheme("https")
.host("ocr.captchaai.com")
.addPathSegment("res.php")
.addQueryParameter("key", apiKey)
.addQueryParameter("action", "get")
.addQueryParameter("id", taskId)
.addQueryParameter("json", "1")
.build()
val request = Request.Builder().url(url).build()
val response = client.newCall(request).execute()
val body = response.body?.string() ?: continue
val parsed = json.decodeFromString<ApiResponse>(body)
if (parsed.request == "CAPCHA_NOT_READY") continue
if (parsed.status != 1) throw CaptchaException("Solve failed: ${parsed.request}")
return parsed.request
}
throw CaptchaException("Timeout waiting for solution")
}
fun checkBalance(): Double {
val url = HttpUrl.Builder()
.scheme("https")
.host("ocr.captchaai.com")
.addPathSegment("res.php")
.addQueryParameter("key", apiKey)
.addQueryParameter("action", "getbalance")
.addQueryParameter("json", "1")
.build()
val request = Request.Builder().url(url).build()
val response = client.newCall(request).execute()
val body = response.body?.string() ?: throw CaptchaException("Empty response")
val parsed = json.decodeFromString<ApiResponse>(body)
return parsed.request.toDouble()
}
}
// Usage
fun main() {
val solver = CaptchaSolver("YOUR_API_KEY")
val balance = solver.checkBalance()
println("Balance: $$balance")
val token = solver.solve(
CaptchaTask.RecaptchaV2(
sitekey = "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageUrl = "https://example.com/login"
)
)
println("Token: ${token.take(50)}...")
}
Method 2: Ktor Client with Coroutines (Recommended)
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.*
import kotlinx.serialization.json.Json
class AsyncCaptchaSolver(private val apiKey: String) {
private val json = Json { ignoreUnknownKeys = true }
private val client = HttpClient(CIO) {
install(ContentNegotiation) {
json(json)
}
engine {
requestTimeout = 30_000
}
}
private val baseUrl = "https://ocr.captchaai.com"
suspend fun solve(task: CaptchaTask): String {
val taskId = submit(task)
return poll(taskId)
}
private suspend fun submit(task: CaptchaTask): String {
val response = client.submitForm(
url = "$baseUrl/in.php",
formParameters = parameters {
append("key", apiKey)
append("json", "1")
when (task) {
is CaptchaTask.RecaptchaV2 -> {
append("method", "userrecaptcha")
append("googlekey", task.sitekey)
append("pageurl", task.pageUrl)
}
is CaptchaTask.RecaptchaV3 -> {
append("method", "userrecaptcha")
append("googlekey", task.sitekey)
append("pageurl", task.pageUrl)
append("version", "v3")
append("action", task.action)
append("min_score", task.minScore.toString())
}
is CaptchaTask.Turnstile -> {
append("method", "turnstile")
append("key", task.sitekey)
append("pageurl", task.pageUrl)
}
is CaptchaTask.ImageBase64 -> {
append("method", "base64")
append("body", task.body)
}
}
}
)
val parsed = json.decodeFromString<ApiResponse>(response.bodyAsText())
if (parsed.status != 1) throw CaptchaException("Submit: ${parsed.request}")
return parsed.request
}
private suspend fun poll(taskId: String, maxWaitMs: Long = 300_000): String {
val deadline = System.currentTimeMillis() + maxWaitMs
while (System.currentTimeMillis() < deadline) {
delay(5_000)
val response = client.get("$baseUrl/res.php") {
parameter("key", apiKey)
parameter("action", "get")
parameter("id", taskId)
parameter("json", "1")
}
val parsed = json.decodeFromString<ApiResponse>(response.bodyAsText())
if (parsed.request == "CAPCHA_NOT_READY") continue
if (parsed.status != 1) throw CaptchaException("Solve: ${parsed.request}")
return parsed.request
}
throw CaptchaException("Timeout")
}
fun close() = client.close()
}
// Usage
fun main() = runBlocking {
val solver = AsyncCaptchaSolver("YOUR_API_KEY")
try {
val token = solver.solve(
CaptchaTask.RecaptchaV2(
sitekey = "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageUrl = "https://example.com/login"
)
)
println("Token: ${token.take(50)}...")
} finally {
solver.close()
}
}
Concurrent Solving with Coroutines
import kotlinx.coroutines.*
suspend fun solveBatch(
solver: AsyncCaptchaSolver,
tasks: List<CaptchaTask>
): List<Result<String>> = coroutineScope {
tasks.map { task ->
async {
runCatching { solver.solve(task) }
}
}.awaitAll()
}
fun main() = runBlocking {
val solver = AsyncCaptchaSolver("YOUR_API_KEY")
val tasks = listOf(
CaptchaTask.RecaptchaV2("KEY_A", "https://site-a.com"),
CaptchaTask.Turnstile("KEY_B", "https://site-b.com"),
CaptchaTask.RecaptchaV2("KEY_C", "https://site-c.com"),
)
val results = solveBatch(solver, tasks)
results.forEachIndexed { i, result ->
result.fold(
onSuccess = { println("Task $i: ${it.take(50)}...") },
onFailure = { println("Task $i failed: ${it.message}") }
)
}
solver.close()
}
Error Handling with Retry
suspend fun solveWithRetry(
solver: AsyncCaptchaSolver,
task: CaptchaTask,
maxRetries: Int = 3
): String {
val retryableErrors = setOf(
"ERROR_NO_SLOT_AVAILABLE",
"ERROR_CAPTCHA_UNSOLVABLE"
)
var lastException: Exception? = null
repeat(maxRetries + 1) { attempt ->
if (attempt > 0) {
val delayMs = (1000L * Math.pow(2.0, attempt.toDouble())).toLong()
println("Retry $attempt/$maxRetries after ${delayMs}ms")
delay(delayMs)
}
try {
return solver.solve(task)
} catch (e: CaptchaException) {
lastException = e
val isRetryable = retryableErrors.any { e.message?.contains(it) == true }
if (!isRetryable) throw e
}
}
throw lastException ?: CaptchaException("Max retries exceeded")
}
Android Integration
// In a ViewModel
class CaptchaViewModel : ViewModel() {
private val solver = AsyncCaptchaSolver("YOUR_API_KEY")
private val _token = MutableLiveData<String>()
val token: LiveData<String> = _token
private val _error = MutableLiveData<String>()
val error: LiveData<String> = _error
fun solveCaptcha(sitekey: String, pageUrl: String) {
viewModelScope.launch {
try {
val result = solver.solve(
CaptchaTask.RecaptchaV2(sitekey, pageUrl)
)
_token.postValue(result)
} catch (e: Exception) {
_error.postValue(e.message)
}
}
}
override fun onCleared() {
super.onCleared()
solver.close()
}
}
Spring Boot Integration
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.*
@Service
class CaptchaService {
private val solver = CaptchaSolver("YOUR_API_KEY")
fun solveRecaptcha(sitekey: String, pageUrl: String): String {
return solver.solve(CaptchaTask.RecaptchaV2(sitekey, pageUrl))
}
}
@RestController
@RequestMapping("/api/captcha")
class CaptchaController(private val service: CaptchaService) {
@PostMapping("/solve")
fun solve(@RequestBody request: SolveRequest): Map<String, String> {
val token = service.solveRecaptcha(request.sitekey, request.pageUrl)
return mapOf("token" to token)
}
data class SolveRequest(val sitekey: String, val pageUrl: String)
}
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
ERROR_WRONG_USER_KEY |
Invalid API key | Verify key at dashboard |
ERROR_ZERO_BALANCE |
No funds | Top up account |
UnresolvedReference |
Missing dependency | Check Gradle/Maven config |
SocketTimeoutException |
Network timeout | Increase OkHttp/Ktor timeout |
JsonDecodingException |
Unexpected API response | Set ignoreUnknownKeys = true |
| Coroutine cancellation | Scope cancelled early | Use supervisorScope for batch |
FAQ
Does CaptchaAI have a Kotlin SDK?
CaptchaAI provides a REST API. The examples here use standard Kotlin libraries (OkHttp, Ktor) for idiomatic integration.
Should I use OkHttp or Ktor?
Use OkHttp for JVM-only projects and when you want simple blocking calls. Use Ktor for coroutine-based code, multiplatform projects, or Android apps.
Can I use this on Android?
Yes. Use the Ktor client with coroutines and call solve() from a ViewModel's viewModelScope. The network calls run on background dispatchers automatically.
How do I handle concurrent solves?
Use coroutineScope + async as shown in the batch example. Kotlin coroutines handle thousands of concurrent operations efficiently.
Related Guides
- Solving CAPTCHAs with Java
- Solving CAPTCHAs with Scala
- Solving CAPTCHAs with Rust
- CaptchaAI API Documentation
Solve CAPTCHAs idiomatically in Kotlin — get your API key and integrate today.
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.