API Tutorials

Solving CAPTCHAs with Ruby and CaptchaAI API

Ruby developers building web scrapers, automation tools, or testing frameworks hit CAPTCHAs constantly. Whether you use Rails, Sinatra, or standalone scripts, CaptchaAI's HTTP API integrates cleanly with Ruby's standard library and popular HTTP gems.

This guide covers reCAPTCHA v2, Cloudflare Turnstile, and image CAPTCHA solving using net/http, Faraday, and HTTParty — with production-ready classes you can drop into any Ruby project.


Why Ruby for CAPTCHA Automation

Ruby's clean syntax and rich gem ecosystem make it ideal for automation scripts. Combined with CaptchaAI's REST API, you get:

  • Simple HTTP integrationnet/http ships with Ruby, no extra dependencies
  • Gem flexibility — Faraday, HTTParty, or RestClient all work
  • Concurrent solving — Ruby threads handle parallel CAPTCHA requests well
  • Rails/Sinatra compatibility — embed solving into web applications directly

Prerequisites

  • Ruby 2.7+ (3.x recommended)
  • CaptchaAI API key (get one here)
  • Target site URL and sitekey for token CAPTCHAs

Install optional gems:

gem install faraday
gem install httparty
gem install nokogiri   # for sitekey extraction

CaptchaAI API Flow

Every CAPTCHA solve follows two steps:

  1. Submit — POST task to https://ocr.captchaai.com/in.php → receive task ID
  2. Poll — GET https://ocr.captchaai.com/res.php?action=get&id=TASK_ID → receive token

Method 1: net/http (Standard Library)

Zero dependencies — works with any Ruby installation.

Basic reCAPTCHA v2 Solver

require 'net/http'
require 'uri'
require 'json'

class CaptchaSolver
  BASE_URL = 'https://ocr.captchaai.com'
  POLL_INTERVAL = 5
  MAX_ATTEMPTS = 60

  def initialize(api_key)
    @api_key = api_key
  end

  # Submit reCAPTCHA v2 task
  def solve_recaptcha_v2(site_url, sitekey)
    task_id = submit_task(
      method: 'userrecaptcha',
      googlekey: sitekey,
      pageurl: site_url
    )
    poll_result(task_id)
  end

  # Submit Cloudflare Turnstile task
  def solve_turnstile(site_url, sitekey)
    task_id = submit_task(
      method: 'turnstile',
      key: sitekey,
      pageurl: site_url
    )
    poll_result(task_id)
  end

  # Submit image CAPTCHA (base64)
  def solve_image(image_base64)
    task_id = submit_task(
      method: 'base64',
      body: image_base64
    )
    poll_result(task_id)
  end

  private

  def submit_task(params)
    uri = URI("#{BASE_URL}/in.php")
    payload = { key: @api_key, json: 1 }.merge(params)

    response = Net::HTTP.post_form(uri, payload)
    data = JSON.parse(response.body)

    raise "Submit failed: #{data['request']}" unless data['status'] == 1
    data['request']
  end

  def poll_result(task_id)
    uri = URI("#{BASE_URL}/res.php")
    params = { key: @api_key, action: 'get', id: task_id, json: 1 }

    MAX_ATTEMPTS.times do
      sleep(POLL_INTERVAL)

      uri.query = URI.encode_www_form(params)
      response = Net::HTTP.get_response(uri)
      data = JSON.parse(response.body)

      next if data['request'] == 'CAPCHA_NOT_READY'
      raise "Solve failed: #{data['request']}" unless data['status'] == 1

      return data['request']
    end

    raise 'Timeout: CAPTCHA not solved within time limit'
  end
end

# Usage
solver = CaptchaSolver.new('YOUR_API_KEY')

token = solver.solve_recaptcha_v2(
  'https://example.com/login',
  '6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-'
)
puts "reCAPTCHA token: #{token[0..50]}..."

Submitting the Token

require 'net/http'

uri = URI('https://example.com/login')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

request = Net::HTTP::Post.new(uri)
request.set_form_data(
  'username' => 'user@example.com',
  'password' => 'password',
  'g-recaptcha-response' => token
)

response = http.request(request)
puts "Login response: #{response.code}"

Faraday provides middleware, retries, and connection pooling.

require 'faraday'
require 'json'

class FaradayCaptchaSolver
  BASE_URL = 'https://ocr.captchaai.com'

  def initialize(api_key)
    @api_key = api_key
    @conn = Faraday.new(url: BASE_URL) do |f|
      f.request :url_encoded
      f.response :json, content_type: /\bjson$/
      f.adapter Faraday.default_adapter
      f.options.timeout = 10
      f.options.open_timeout = 5
    end
  end

  def solve_recaptcha_v2(site_url, sitekey)
    task_id = submit(
      method: 'userrecaptcha',
      googlekey: sitekey,
      pageurl: site_url
    )
    poll(task_id)
  end

  def solve_turnstile(site_url, sitekey)
    task_id = submit(
      method: 'turnstile',
      key: sitekey,
      pageurl: site_url
    )
    poll(task_id)
  end

  def solve_recaptcha_v3(site_url, sitekey, action: 'verify', min_score: 0.7)
    task_id = submit(
      method: 'userrecaptcha',
      googlekey: sitekey,
      pageurl: site_url,
      version: 'v3',
      action: action,
      min_score: min_score
    )
    poll(task_id)
  end

  def check_balance
    response = @conn.get('/res.php', key: @api_key, action: 'getbalance', json: 1)
    response.body['request'].to_f
  end

  private

  def submit(params)
    response = @conn.post('/in.php', { key: @api_key, json: 1 }.merge(params))
    data = response.body

    unless data['status'] == 1
      raise "Submit error: #{data['request']}"
    end

    data['request']
  end

  def poll(task_id, max_wait: 300, interval: 5)
    deadline = Time.now + max_wait

    loop do
      raise 'Timeout waiting for CAPTCHA solution' if Time.now > deadline
      sleep(interval)

      response = @conn.get('/res.php', key: @api_key, action: 'get', id: task_id, json: 1)
      data = response.body

      next if data['request'] == 'CAPCHA_NOT_READY'
      raise "Solve error: #{data['request']}" unless data['status'] == 1

      return data['request']
    end
  end
end

# Usage
solver = FaradayCaptchaSolver.new('YOUR_API_KEY')

balance = solver.check_balance
puts "Balance: $#{balance}"

token = solver.solve_recaptcha_v2(
  'https://example.com/form',
  '6LdKlZEpAAAAAAOQjzC2v_d36tWxCl6dWsozdSy9'
)
puts "Token: #{token[0..50]}..."

Method 3: HTTParty (Simplest Syntax)

HTTParty is ideal for quick scripts.

require 'httparty'

class HTTPartySolver
  include HTTParty
  base_uri 'https://ocr.captchaai.com'

  def initialize(api_key)
    @api_key = api_key
  end

  def solve_recaptcha_v2(site_url, sitekey)
    response = self.class.post('/in.php', body: {
      key: @api_key,
      method: 'userrecaptcha',
      googlekey: sitekey,
      pageurl: site_url,
      json: 1
    })

    data = response.parsed_response
    raise "Submit failed: #{data['request']}" unless data['status'] == 1

    poll_result(data['request'])
  end

  def solve_image(image_path)
    image_data = Base64.strict_encode64(File.read(image_path, mode: 'rb'))

    response = self.class.post('/in.php', body: {
      key: @api_key,
      method: 'base64',
      body: image_data,
      json: 1
    })

    data = response.parsed_response
    raise "Submit failed: #{data['request']}" unless data['status'] == 1

    poll_result(data['request'])
  end

  private

  def poll_result(task_id)
    60.times do
      sleep 5

      response = self.class.get('/res.php', query: {
        key: @api_key,
        action: 'get',
        id: task_id,
        json: 1
      })

      data = response.parsed_response
      next if data['request'] == 'CAPCHA_NOT_READY'
      raise "Solve failed: #{data['request']}" unless data['status'] == 1

      return data['request']
    end

    raise 'CAPTCHA solve timeout'
  end
end

# Usage
solver = HTTPartySolver.new('YOUR_API_KEY')
token = solver.solve_recaptcha_v2('https://example.com', 'SITEKEY_HERE')
puts token

Sitekey Extraction with Nokogiri

require 'nokogiri'
require 'open-uri'

def extract_sitekey(url)
  doc = Nokogiri::HTML(URI.open(url))

  # reCAPTCHA
  recaptcha = doc.at_css('[data-sitekey]')
  return { type: 'recaptcha', key: recaptcha['data-sitekey'] } if recaptcha

  # Turnstile
  turnstile = doc.at_css('.cf-turnstile[data-sitekey]')
  return { type: 'turnstile', key: turnstile['data-sitekey'] } if turnstile

  # Check scripts for sitekey patterns
  doc.css('script').each do |script|
    text = script.text
    if (match = text.match(/sitekey['":\s]+['"]([A-Za-z0-9_-]{20,})['"]/))
      return { type: 'unknown', key: match[1] }
    end
  end

  nil
end

result = extract_sitekey('https://example.com/protected')
puts "Found #{result[:type]} sitekey: #{result[:key]}" if result

Concurrent Solving with Threads

Solve multiple CAPTCHAs in parallel using Ruby threads:

require 'thread'

solver = CaptchaSolver.new('YOUR_API_KEY')

tasks = [
  { url: 'https://site-a.com', key: 'SITEKEY_A' },
  { url: 'https://site-b.com', key: 'SITEKEY_B' },
  { url: 'https://site-c.com', key: 'SITEKEY_C' }
]

mutex = Mutex.new
results = {}

threads = tasks.map do |task|
  Thread.new do
    begin
      token = solver.solve_recaptcha_v2(task[:url], task[:key])
      mutex.synchronize { results[task[:url]] = token }
    rescue => e
      mutex.synchronize { results[task[:url]] = "Error: #{e.message}" }
    end
  end
end

threads.each(&:join)

results.each do |url, result|
  puts "#{url}: #{result[0..50]}..."
end

Error Handling Best Practices

class RobustSolver
  MAX_RETRIES = 3
  RETRY_ERRORS = %w[
    ERROR_NO_SLOT_AVAILABLE
    ERROR_CAPTCHA_UNSOLVABLE
    CAPCHA_NOT_READY
  ]

  def initialize(api_key)
    @solver = CaptchaSolver.new(api_key)
  end

  def solve_with_retry(site_url, sitekey, type: :recaptcha_v2)
    retries = 0

    begin
      case type
      when :recaptcha_v2
        @solver.solve_recaptcha_v2(site_url, sitekey)
      when :turnstile
        @solver.solve_turnstile(site_url, sitekey)
      end
    rescue => e
      retries += 1
      if retries <= MAX_RETRIES && retryable?(e.message)
        delay = 2**retries + rand(0..2)
        puts "Retry #{retries}/#{MAX_RETRIES} after #{delay}s: #{e.message}"
        sleep(delay)
        retry
      end
      raise
    end
  end

  private

  def retryable?(message)
    RETRY_ERRORS.any? { |err| message.include?(err) }
  end
end

Rails Integration Example

# app/services/captcha_service.rb
class CaptchaService
  def initialize
    @api_key = Rails.application.credentials.captchaai_api_key
    @solver = FaradayCaptchaSolver.new(@api_key)
  end

  def verify_and_solve(site_url, sitekey)
    Rails.cache.fetch("captcha:#{sitekey}", expires_in: 90.seconds) do
      @solver.solve_recaptcha_v2(site_url, sitekey)
    end
  end
end

# In a controller
class ScrapingController < ApplicationController
  def create
    service = CaptchaService.new
    token = service.verify_and_solve(params[:url], params[:sitekey])
    render json: { token: token }
  rescue => e
    render json: { error: e.message }, status: :unprocessable_entity
  end
end

Troubleshooting

Error Cause Fix
ERROR_WRONG_USER_KEY Invalid API key Verify key at dashboard
ERROR_ZERO_BALANCE No funds Top up account
ERROR_NO_SLOT_AVAILABLE Server busy Retry after 5 seconds
ERROR_CAPTCHA_UNSOLVABLE Failed solving Retry with fresh task
SSL_connect error Ruby SSL config Update ca-certificates or set OpenSSL::SSL::VERIFY_PEER
Timeout::Error Network/polling timeout Increase timeout, check connectivity

FAQ

Does CaptchaAI have a Ruby gem?

CaptchaAI provides a REST API that works with any Ruby HTTP library. No official gem is needed — net/http, Faraday, or HTTParty all integrate cleanly.

Which Ruby HTTP library should I use?

Use net/http for zero-dependency scripts, Faraday for production apps needing middleware and retries, or HTTParty for quick prototypes.

Can I use this with Ruby on Rails?

Yes. Wrap the solver in a service object and optionally use Rails.cache to avoid re-solving within the token validity window.

Is Ruby threading safe for parallel solves?

Yes. CaptchaAI API calls are I/O-bound, so Ruby threads handle concurrency well. Use a Mutex for shared state.



Start solving CAPTCHAs in Ruby — get your API key and integrate in minutes.

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.