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 style —
Future,Try,Eitherfor 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)
Method 1: sttp Client (Recommended)
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).
Related Guides
Add CAPTCHA solving to your Scala applications — get your API key and integrate today.
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.