API Tutorials

Solving CAPTCHAs with Kotlin and CaptchaAI API

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)}...")
}

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.



Solve CAPTCHAs idiomatically in Kotlin — get your API key and integrate today.

Full Working Code

Complete runnable examples for this article in Python, Node.js, PHP, Go, Java, C#, Ruby, Rust, Kotlin & Bash.

View on GitHub →

Discussions (0)

No comments yet.