API Tutorials

Solving CAPTCHAs with Scala and CaptchaAI API

Scala developers building data pipelines with Spark, web applications with Play Framework, or distributed systems with Akka encounter CAPTCHAs in web scraping, form automation, and API interactions. CaptchaAI's HTTP API integrates with Scala's rich ecosystem through sttp, Akka HTTP, or Java's HttpClient.

This guide covers reCAPTCHA v2/v3, Cloudflare Turnstile, and image CAPTCHA solving — with both blocking and Future-based async implementations.


Why Scala for CAPTCHA Automation

  • JVM ecosystem — access to all Java libraries plus Scala-native options
  • Functional styleFuture, Try, Either for clean error handling
  • Akka/Pekko — actor-based concurrency for massive parallel solving
  • Spark integration — embed CAPTCHA solving in distributed data pipelines
  • Type safety — sealed traits and case classes model API responses precisely

Prerequisites

build.sbt

libraryDependencies ++= Seq(
  "com.softwaremill.sttp.client3" %% "core" % "3.9.7",
  "com.softwaremill.sttp.client3" %% "circe" % "3.9.7",
  "io.circe" %% "circe-generic" % "0.14.9",
  "io.circe" %% "circe-parser" % "0.14.9"
)

For Akka HTTP:

libraryDependencies += "com.typesafe.akka" %% "akka-http" % "10.6.3"
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % "2.9.3"

Data Models

import io.circe.{Decoder, Encoder}
import io.circe.generic.semiauto._

case class ApiResponse(status: Int, request: String)

object ApiResponse {
  implicit val decoder: Decoder[ApiResponse] = deriveDecoder
}

sealed trait CaptchaTask
object CaptchaTask {
  case class RecaptchaV2(sitekey: String, pageUrl: String) extends CaptchaTask
  case class RecaptchaV3(
    sitekey: String,
    pageUrl: String,
    action: String = "verify",
    minScore: Double = 0.7
  ) extends CaptchaTask
  case class Turnstile(sitekey: String, pageUrl: String) extends CaptchaTask
  case class ImageBase64(body: String) extends CaptchaTask
}

case class CaptchaException(message: String) extends Exception(message)

import sttp.client3._
import io.circe.parser._
import scala.concurrent.duration._

class CaptchaSolver(apiKey: String) {
  private val baseUrl = "https://ocr.captchaai.com"
  private val backend = HttpClientSyncBackend()
  private val pollInterval = 5.seconds
  private val maxWait = 300.seconds

  def solve(task: CaptchaTask): String = {
    val taskId = submit(task)
    poll(taskId)
  }

  def checkBalance(): Double = {
    val response = basicRequest
      .get(uri"$baseUrl/res.php?key=$apiKey&action=getbalance&json=1")
      .send(backend)

    val json = parse(response.body.getOrElse("{}")).getOrElse(
      throw CaptchaException("Invalid response")
    )
    json.hcursor.get[String]("request").getOrElse("0").toDouble
  }

  private def submit(task: CaptchaTask): String = {
    val params: Map[String, String] = Map(
      "key" -> apiKey,
      "json" -> "1"
    ) ++ taskParams(task)

    val response = basicRequest
      .post(uri"$baseUrl/in.php")
      .body(params)
      .send(backend)

    val body = response.body.getOrElse(throw CaptchaException("Empty response"))
    val apiResp = decode[ApiResponse](body).getOrElse(
      throw CaptchaException(s"Parse error: $body")
    )

    if (apiResp.status != 1) throw CaptchaException(s"Submit: ${apiResp.request}")
    apiResp.request
  }

  private def poll(taskId: String): String = {
    val deadline = System.currentTimeMillis() + maxWait.toMillis

    while (System.currentTimeMillis() < deadline) {
      Thread.sleep(pollInterval.toMillis)

      val response = basicRequest
        .get(uri"$baseUrl/res.php?key=$apiKey&action=get&id=$taskId&json=1")
        .send(backend)

      val body = response.body.getOrElse("")
      val apiResp = decode[ApiResponse](body).getOrElse(
        ApiResponse(0, "Parse error")
      )

      if (apiResp.request == "CAPCHA_NOT_READY") ()
      else if (apiResp.status != 1) throw CaptchaException(s"Solve: ${apiResp.request}")
      else return apiResp.request
    }

    throw CaptchaException("Timeout")
  }

  private def taskParams(task: CaptchaTask): Map[String, String] = task match {
    case CaptchaTask.RecaptchaV2(sitekey, pageUrl) =>
      Map("method" -> "userrecaptcha", "googlekey" -> sitekey, "pageurl" -> pageUrl)
    case CaptchaTask.RecaptchaV3(sitekey, pageUrl, action, minScore) =>
      Map(
        "method" -> "userrecaptcha", "googlekey" -> sitekey, "pageurl" -> pageUrl,
        "version" -> "v3", "action" -> action, "min_score" -> minScore.toString
      )
    case CaptchaTask.Turnstile(sitekey, pageUrl) =>
      Map("method" -> "turnstile", "key" -> sitekey, "pageurl" -> pageUrl)
    case CaptchaTask.ImageBase64(body) =>
      Map("method" -> "base64", "body" -> body)
  }

  def close(): Unit = backend.close()
}

Usage

object Main extends App {
  val solver = new CaptchaSolver("YOUR_API_KEY")

  try {
    val balance = solver.checkBalance()
    println(f"Balance: $$$balance%.2f")

    val token = solver.solve(CaptchaTask.RecaptchaV2(
      sitekey = "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
      pageUrl = "https://example.com/login"
    ))
    println(s"Token: ${token.take(50)}...")

    val turnstile = solver.solve(CaptchaTask.Turnstile(
      sitekey = "0x4AAAAAAAB5...",
      pageUrl = "https://example.com/form"
    ))
    println(s"Turnstile: ${turnstile.take(50)}...")

  } finally {
    solver.close()
  }
}

Async Solver with Futures

import scala.concurrent.{Future, ExecutionContext, Await, blocking}
import scala.concurrent.duration._
import sttp.client3._
import io.circe.parser._

class AsyncCaptchaSolver(apiKey: String)(implicit ec: ExecutionContext) {
  private val baseUrl = "https://ocr.captchaai.com"
  private val backend = HttpClientSyncBackend()

  def solve(task: CaptchaTask): Future[String] = Future {
    blocking {
      val taskId = submit(task)
      poll(taskId)
    }
  }

  def solveBatch(tasks: Seq[CaptchaTask]): Future[Seq[Either[Throwable, String]]] = {
    val futures = tasks.map { task =>
      solve(task).map(Right(_)).recover { case e => Left(e) }
    }
    Future.sequence(futures)
  }

  private def submit(task: CaptchaTask): String = {
    val params = Map("key" -> apiKey, "json" -> "1") ++ taskToParams(task)
    val response = basicRequest.post(uri"$baseUrl/in.php").body(params).send(backend)
    val body = response.body.getOrElse(throw CaptchaException("Empty"))
    val resp = decode[ApiResponse](body).getOrElse(throw CaptchaException(body))
    if (resp.status != 1) throw CaptchaException(s"Submit: ${resp.request}")
    resp.request
  }

  private def poll(taskId: String): String = {
    val deadline = System.currentTimeMillis() + 300000L
    while (System.currentTimeMillis() < deadline) {
      Thread.sleep(5000)
      val response = basicRequest
        .get(uri"$baseUrl/res.php?key=$apiKey&action=get&id=$taskId&json=1")
        .send(backend)
      val body = response.body.getOrElse("")
      decode[ApiResponse](body).toOption match {
        case Some(r) if r.request == "CAPCHA_NOT_READY" => ()
        case Some(r) if r.status == 1 => return r.request
        case Some(r) => throw CaptchaException(s"Solve: ${r.request}")
        case None => ()
      }
    }
    throw CaptchaException("Timeout")
  }

  private def taskToParams(task: CaptchaTask): Map[String, String] = task match {
    case CaptchaTask.RecaptchaV2(sk, url) =>
      Map("method" -> "userrecaptcha", "googlekey" -> sk, "pageurl" -> url)
    case CaptchaTask.RecaptchaV3(sk, url, action, score) =>
      Map("method" -> "userrecaptcha", "googlekey" -> sk, "pageurl" -> url,
        "version" -> "v3", "action" -> action, "min_score" -> score.toString)
    case CaptchaTask.Turnstile(sk, url) =>
      Map("method" -> "turnstile", "key" -> sk, "pageurl" -> url)
    case CaptchaTask.ImageBase64(body) =>
      Map("method" -> "base64", "body" -> body)
  }

  def close(): Unit = backend.close()
}

Usage

import scala.concurrent.ExecutionContext.Implicits.global

object AsyncMain extends App {
  val solver = new AsyncCaptchaSolver("YOUR_API_KEY")

  val tasks = Seq(
    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 = Await.result(solver.solveBatch(tasks), 10.minutes)

  results.zipWithIndex.foreach { case (result, i) =>
    result match {
      case Right(token) => println(s"Task $i: ${token.take(50)}...")
      case Left(error) => println(s"Task $i failed: ${error.getMessage}")
    }
  }

  solver.close()
}

Image CAPTCHA Solving

import java.util.Base64
import java.nio.file.{Files, Paths}

def solveImageFile(solver: CaptchaSolver, path: String): String = {
  val bytes = Files.readAllBytes(Paths.get(path))
  val encoded = Base64.getEncoder.encodeToString(bytes)
  solver.solve(CaptchaTask.ImageBase64(encoded))
}

// Usage
val text = solveImageFile(solver, "captcha.png")
println(s"Text: $text")

Error Handling with Retry

import scala.util.{Try, Success, Failure}

def solveWithRetry(
  solver: CaptchaSolver,
  task: CaptchaTask,
  maxRetries: Int = 3
): String = {
  val retryable = Set("ERROR_NO_SLOT_AVAILABLE", "ERROR_CAPTCHA_UNSOLVABLE")

  var lastError: Throwable = new CaptchaException("No attempts")

  for (attempt <- 0 to maxRetries) {
    if (attempt > 0) {
      val delay = math.pow(2, attempt).toLong * 1000 + (math.random() * 2000).toLong
      println(s"Retry $attempt/$maxRetries after ${delay}ms")
      Thread.sleep(delay)
    }

    Try(solver.solve(task)) match {
      case Success(token) => return token
      case Failure(e: CaptchaException) if retryable.exists(e.message.contains) =>
        lastError = e
      case Failure(e) => throw e
    }
  }

  throw lastError
}

Play Framework Integration

// app/services/CaptchaService.scala
import javax.inject._
import play.api.Configuration

@Singleton
class CaptchaService @Inject()(config: Configuration) {
  private val apiKey = config.get[String]("captchaai.apiKey")
  private val solver = new CaptchaSolver(apiKey)

  def solveRecaptcha(sitekey: String, pageUrl: String): String = {
    solver.solve(CaptchaTask.RecaptchaV2(sitekey, pageUrl))
  }

  def solveTurnstile(sitekey: String, pageUrl: String): String = {
    solver.solve(CaptchaTask.Turnstile(sitekey, pageUrl))
  }
}

// app/controllers/CaptchaController.scala
import javax.inject._
import play.api.mvc._
import play.api.libs.json._

@Singleton
class CaptchaController @Inject()(
  cc: ControllerComponents,
  service: CaptchaService
) extends AbstractController(cc) {

  case class SolveRequest(sitekey: String, pageUrl: String)
  implicit val reads: Reads[SolveRequest] = Json.reads[SolveRequest]

  def solve(): Action[JsValue] = Action(parse.json) { request =>
    request.body.validate[SolveRequest].fold(
      errors => BadRequest(Json.obj("error" -> "Invalid request")),
      req => {
        try {
          val token = service.solveRecaptcha(req.sitekey, req.pageUrl)
          Ok(Json.obj("token" -> token))
        } catch {
          case e: CaptchaException =>
            InternalServerError(Json.obj("error" -> e.message))
        }
      }
    )
  }
}

Spark Integration

import org.apache.spark.sql.{SparkSession, DataFrame}

def scrapeWithCaptcha(
  spark: SparkSession,
  urls: Seq[String],
  apiKey: String,
  sitekey: String
): DataFrame = {
  import spark.implicits._

  val results = urls.map { url =>
    try {
      val solver = new CaptchaSolver(apiKey)
      val token = solver.solve(CaptchaTask.RecaptchaV2(sitekey, url))
      solver.close()

      // Use token to fetch data
      (url, token.take(50), "success")
    } catch {
      case e: Exception => (url, e.getMessage, "failed")
    }
  }

  results.toDF("url", "result", "status")
}

Troubleshooting

Error Cause Fix
ERROR_WRONG_USER_KEY Invalid API key Verify key at dashboard
ERROR_ZERO_BALANCE No funds Top up account
ConnectionException Network issue Check connectivity, increase timeout
DecodingFailure Unexpected JSON field Add missing fields to case class
ClassNotFoundException Missing dependency Check build.sbt dependencies
TimeoutException Slow solve Increase maxWait duration

FAQ

Does CaptchaAI have a Scala library?

CaptchaAI provides a REST API. The sttp + circe combination shown here gives idiomatic Scala integration.

Should I use blocking or async?

Use blocking (CaptchaSolver) for simple scripts. Use async (AsyncCaptchaSolver with Futures) for production applications, especially with Play or Akka.

Can I use this with Akka Actors?

Yes. Wrap the async solver in an actor or use Akka HTTP's client API directly.

Does this work with Scala 3?

Yes. The code works with both Scala 2.13 and Scala 3. Update circe imports for Scala 3 derivation (derives Decoder).



Add CAPTCHA solving to your Scala applications — 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.