CAPSOLVER
Blog
Integrating Katana with CapSolver: Automated CAPTCHA Solving for Web Crawling

Integrating Katana with CapSolver: Automated CAPTCHA Solving for Web Crawling

Logo of CapSolver

Lucas Mitchell

Automation Engineer

09-Jan-2026

How to Solve Captcha with Katana Using CapSolver

Web crawling is an essential technique for security researchers, penetration testers, and data analysts. However, modern websites increasingly employ CAPTCHAs to protect against automated access. This guide demonstrates how to integrate Katana, ProjectDiscovery's powerful web crawler, with CapSolver, a leading CAPTCHA solving service, to create a robust crawling solution that handles CAPTCHA challenges automatically.

What You Will Learn

  • Setting up Katana in headless browser mode
  • Integrating Capsolver's API for automated CAPTCHA solving
  • Handling reCAPTCHA v2 and Cloudflare Turnstile
  • Complete, runnable code examples for each CAPTCHA type
  • Best practices for efficient and responsible crawling

What is Katana?

Katana is a next-generation web crawling framework developed by ProjectDiscovery. It's designed for speed and flexibility, making it ideal for security reconnaissance and automation pipelines.

Key Features

  • Dual Crawling Modes: Standard HTTP-based crawling and headless browser automation
  • JavaScript Support: Parse and crawl JavaScript-rendered content
  • Flexible Configuration: Custom headers, cookies, form filling, and scope control
  • Multiple Output Formats: Plain text, JSON, or JSONL

Installation

bash Copy
# Requires Go 1.24+
CGO_ENABLED=1 go install github.com/projectdiscovery/katana/cmd/katana@latest

Basic Usage

bash Copy
katana -u https://example.com -headless

What is Capsolver?

CapSolver is an AI-powered CAPTCHA solving service that provides fast and reliable solutions for various CAPTCHA types.

Supported CAPTCHA Types

  • reCAPTCHA: v2 and Enterprise versions
  • Cloudflare: Turnstile and Challenge
  • AWS WAF: WAF protection bypass
  • And More

API Workflow

Capsolver uses a task-based API model:

  1. Create Task: Submit CAPTCHA parameters (type, siteKey, URL)
  2. Get Task ID: Receive a unique task identifier
  3. Poll for Result: Check task status until solution is ready
  4. Receive Token: Get the solved CAPTCHA token

Prerequisites

Before starting, ensure you have:

  1. Go 1.24+ installed
  2. Capsolver API Key - Sign up here
  3. Chrome Browser (for headless mode)

Set your API key as an environment variable:

bash Copy
export CAPSOLVER_API_KEY="YOUR_API_KEY"

Integration Architecture

Copy
┌─────────────────────────┐
│   Go Application        │
│   (go-rod browser)      │
└───────────┬─────────────┘
            │
            ▼
┌─────────────────────────┐
│   Target Website        │
│   (with CAPTCHA)        │
└───────────┬─────────────┘
            │
    CAPTCHA Detected
            │
            ▼
┌─────────────────────────┐
│   Extract Parameters    │
│   (siteKey, URL, type)  │
└───────────┬─────────────┘
            │
            ▼
┌─────────────────────────┐
│   Capsolver API         │
│   createTask()          │
└───────────┬─────────────┘
            │
            ▼
┌─────────────────────────┐
│   Poll for Result       │
│   getTaskResult()       │
└───────────┬─────────────┘
            │
            ▼
┌─────────────────────────┐
│   Inject Token          │
│   into Page             │
└───────────┬─────────────┘
            │
            ▼
┌─────────────────────────┐
│   Continue Crawling     │
└─────────────────────────┘

Solving reCAPTCHA v2 with CapSolver

reCAPTCHA v2 is the most common CAPTCHA type, displaying an "I'm not a robot" checkbox or image challenges. Here's a complete, runnable script to solve reCAPTCHA v2:

go Copy
// reCAPTCHA v2 Solver - Complete Example
// Usage: go run main.go
// Requires: CAPSOLVER_API_KEY environment variable

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/go-rod/rod"
	"github.com/go-rod/rod/lib/launcher"
)

// Configuration
var (
	CAPSOLVER_API_KEY = os.Getenv("CAPSOLVER_API_KEY")
	CAPSOLVER_API     = "https://api.capsolver.com"
)

// API Response structures
type CreateTaskResponse struct {
	ErrorID          int    `json:"errorId"`
	ErrorCode        string `json:"errorCode"`
	ErrorDescription string `json:"errorDescription"`
	TaskID           string `json:"taskId"`
}

type GetTaskResultResponse struct {
	ErrorID          int    `json:"errorId"`
	ErrorCode        string `json:"errorCode"`
	ErrorDescription string `json:"errorDescription"`
	Status           string `json:"status"`
	Solution         struct {
		GRecaptchaResponse string `json:"gRecaptchaResponse"`
	} `json:"solution"`
}

type BalanceResponse struct {
	ErrorID int     `json:"errorId"`
	Balance float64 `json:"balance"`
}

// CapsolverClient handles API communication
type CapsolverClient struct {
	APIKey string
	Client *http.Client
}

// NewCapsolverClient creates a new Capsolver client
func NewCapsolverClient(apiKey string) *CapsolverClient {
	return &CapsolverClient{
		APIKey: apiKey,
		Client: &http.Client{Timeout: 120 * time.Second},
	}
}

// GetBalance retrieves account balance
func (c *CapsolverClient) GetBalance() (float64, error) {
	payload := map[string]string{"clientKey": c.APIKey}
	jsonData, _ := json.Marshal(payload)

	resp, err := c.Client.Post(CAPSOLVER_API+"/getBalance", "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return 0, err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	var result BalanceResponse
	json.Unmarshal(body, &result)

	if result.ErrorID != 0 {
		return 0, fmt.Errorf("balance check failed")
	}

	return result.Balance, nil
}

// SolveRecaptchaV2 solves a reCAPTCHA v2 challenge
func (c *CapsolverClient) SolveRecaptchaV2(websiteURL, siteKey string) (string, error) {
	log.Printf("Creating reCAPTCHA v2 task for %s", websiteURL)

	// Create task
	task := map[string]interface{}{
		"type":       "ReCaptchaV2TaskProxyLess",
		"websiteURL": websiteURL,
		"websiteKey": siteKey,
	}

	payload := map[string]interface{}{
		"clientKey": c.APIKey,
		"task":      task,
	}

	jsonData, _ := json.Marshal(payload)
	resp, err := c.Client.Post(CAPSOLVER_API+"/createTask", "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return "", fmt.Errorf("failed to create task: %w", err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	var createResult CreateTaskResponse
	json.Unmarshal(body, &createResult)

	if createResult.ErrorID != 0 {
		return "", fmt.Errorf("API error: %s - %s", createResult.ErrorCode, createResult.ErrorDescription)
	}

	log.Printf("Task created: %s", createResult.TaskID)

	// Poll for result
	for i := 0; i < 120; i++ {
		result, err := c.getTaskResult(createResult.TaskID)
		if err != nil {
			return "", err
		}

		if result.Status == "ready" {
			log.Printf("CAPTCHA solved successfully!")
			return result.Solution.GRecaptchaResponse, nil
		}

		if result.Status == "failed" {
			return "", fmt.Errorf("task failed: %s", result.ErrorDescription)
		}

		if i%10 == 0 {
			log.Printf("Waiting for solution... (%ds)", i)
		}
		time.Sleep(1 * time.Second)
	}

	return "", fmt.Errorf("timeout waiting for solution")
}

func (c *CapsolverClient) getTaskResult(taskID string) (*GetTaskResultResponse, error) {
	payload := map[string]string{
		"clientKey": c.APIKey,
		"taskId":    taskID,
	}

	jsonData, _ := json.Marshal(payload)
	resp, err := c.Client.Post(CAPSOLVER_API+"/getTaskResult", "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	var result GetTaskResultResponse
	json.Unmarshal(body, &result)

	return &result, nil
}

// extractSiteKey extracts the reCAPTCHA site key from page HTML
func extractSiteKey(html string) string {
	// Look for data-sitekey attribute
	patterns := []string{
		`data-sitekey="`,
		`data-sitekey='`,
		`"sitekey":"`,
		`'sitekey':'`,
	}

	for _, pattern := range patterns {
		if idx := strings.Index(html, pattern); idx != -1 {
			start := idx + len(pattern)
			end := start
			for end < len(html) && html[end] != '"' && html[end] != '\'' {
				end++
			}
			if end > start {
				return html[start:end]
			}
		}
	}
	return ""
}

// injectRecaptchaToken injects the solved token into the page
func injectRecaptchaToken(page *rod.Page, token string) error {
	js := fmt.Sprintf(`
		(function() {
			// Set the response textarea
			var responseField = document.getElementById('g-recaptcha-response');
			if (responseField) {
				responseField.style.display = 'block';
				responseField.value = '%s';
			}

			// Also set any hidden textareas
			var textareas = document.querySelectorAll('textarea[name="g-recaptcha-response"]');
			for (var i = 0; i < textareas.length; i++) {
				textareas[i].value = '%s';
			}

			// Trigger callback if it exists
			if (typeof ___grecaptcha_cfg !== 'undefined') {
				var clients = ___grecaptcha_cfg.clients;
				for (var key in clients) {
					var client = clients[key];
					if (client) {
						// Try to find and call the callback
						try {
							var callback = client.callback ||
								(client.Q && client.Q.callback) ||
								(client.S && client.S.callback);
							if (typeof callback === 'function') {
								callback('%s');
							}
						} catch(e) {}
					}
				}
			}

			return true;
		})();
	`, token, token, token)

	_, err := page.Eval(js)
	return err
}

func main() {
	// Check API key
	if CAPSOLVER_API_KEY == "" {
		log.Fatal("CAPSOLVER_API_KEY environment variable is required")
	}

	// Target URL - Google's reCAPTCHA demo page
	targetURL := "https://www.google.com/recaptcha/api2/demo"

	log.Println("==============================================")
	log.Println("Katana + Capsolver - reCAPTCHA v2 Demo")
	log.Println("==============================================")

	// Initialize Capsolver client
	client := NewCapsolverClient(CAPSOLVER_API_KEY)

	// Check balance
	balance, err := client.GetBalance()
	if err != nil {
		log.Printf("Warning: Could not check balance: %v", err)
	} else {
		log.Printf("Capsolver balance: $%.2f", balance)
	}

	// Launch browser
	log.Println("Launching browser...")
	path, _ := launcher.LookPath()
	u := launcher.New().Bin(path).Headless(true).MustLaunch()
	browser := rod.New().ControlURL(u).MustConnect()
	defer browser.MustClose()

	// Navigate to target
	log.Printf("Navigating to: %s", targetURL)
	page := browser.MustPage(targetURL)
	page.MustWaitLoad()
	time.Sleep(2 * time.Second)

	// Get page HTML and extract site key
	html := page.MustHTML()

	// Check for reCAPTCHA
	if !strings.Contains(html, "g-recaptcha") && !strings.Contains(html, "grecaptcha") {
		log.Fatal("No reCAPTCHA found on page")
	}

	log.Println("reCAPTCHA detected!")

	// Extract site key
	siteKey := extractSiteKey(html)
	if siteKey == "" {
		log.Fatal("Could not extract site key")
	}
	log.Printf("Site key: %s", siteKey)

	// Solve CAPTCHA
	log.Println("Solving CAPTCHA with Capsolver...")
	token, err := client.SolveRecaptchaV2(targetURL, siteKey)
	if err != nil {
		log.Fatalf("Failed to solve CAPTCHA: %v", err)
	}

	log.Printf("Token received: %s...", token[:50])

	// Inject token
	log.Println("Injecting token into page...")
	err = injectRecaptchaToken(page, token)
	if err != nil {
		log.Fatalf("Failed to inject token: %v", err)
	}

	// Submit form
	log.Println("Submitting form...")
	submitBtn := page.MustElement("#recaptcha-demo-submit")
	submitBtn.MustClick()

	// Wait for result
	time.Sleep(3 * time.Second)

	// Check result
	newHTML := page.MustHTML()
	if strings.Contains(newHTML, "Verification Success") || strings.Contains(newHTML, "success") {
		log.Println("==============================================")
		log.Println("SUCCESS! reCAPTCHA solved and verified!")
		log.Println("==============================================")
	} else {
		log.Println("Form submitted - check page for result")
	}

	// Get page title
	title := page.MustEval(`document.title`).String()
	log.Printf("Final page title: %s", title)
}

Setup and Run

bash Copy
# Create project
mkdir katana-recaptcha-v2
cd katana-recaptcha-v2
go mod init katana-recaptcha-v2

# Install dependencies
go get github.com/go-rod/rod@latest

# Set API key
export CAPSOLVER_API_KEY="YOUR_API_KEY"

# Run
go run main.go

Solving Cloudflare Turnstile with CapSolver

Cloudflare Turnstile is a privacy-focused CAPTCHA alternative. Here's a complete script:

go Copy
// Cloudflare Turnstile Solver - Complete Example
// Usage: go run main.go
// Requires: CAPSOLVER_API_KEY environment variable

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"regexp"
	"strings"
	"time"

	"github.com/go-rod/rod"
	"github.com/go-rod/rod/lib/launcher"
)

// Configuration
var (
	CAPSOLVER_API_KEY = os.Getenv("CAPSOLVER_API_KEY")
	CAPSOLVER_API     = "https://api.capsolver.com"
)

// API Response structures
type CreateTaskResponse struct {
	ErrorID          int    `json:"errorId"`
	ErrorCode        string `json:"errorCode"`
	ErrorDescription string `json:"errorDescription"`
	TaskID           string `json:"taskId"`
}

type GetTaskResultResponse struct {
	ErrorID          int    `json:"errorId"`
	ErrorCode        string `json:"errorCode"`
	ErrorDescription string `json:"errorDescription"`
	Status           string `json:"status"`
	Solution         struct {
		Token string `json:"token"`
	} `json:"solution"`
}

type BalanceResponse struct {
	ErrorID int     `json:"errorId"`
	Balance float64 `json:"balance"`
}

// CapsolverClient handles API communication
type CapsolverClient struct {
	APIKey string
	Client *http.Client
}

// NewCapsolverClient creates a new Capsolver client
func NewCapsolverClient(apiKey string) *CapsolverClient {
	return &CapsolverClient{
		APIKey: apiKey,
		Client: &http.Client{Timeout: 120 * time.Second},
	}
}

// GetBalance retrieves account balance
func (c *CapsolverClient) GetBalance() (float64, error) {
	payload := map[string]string{"clientKey": c.APIKey}
	jsonData, _ := json.Marshal(payload)

	resp, err := c.Client.Post(CAPSOLVER_API+"/getBalance", "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return 0, err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	var result BalanceResponse
	json.Unmarshal(body, &result)

	return result.Balance, nil
}

// SolveTurnstile solves a Cloudflare Turnstile challenge
func (c *CapsolverClient) SolveTurnstile(websiteURL, siteKey string) (string, error) {
	log.Printf("Creating Turnstile task for %s", websiteURL)

	// Create task
	task := map[string]interface{}{
		"type":       "AntiTurnstileTaskProxyLess",
		"websiteURL": websiteURL,
		"websiteKey": siteKey,
	}

	payload := map[string]interface{}{
		"clientKey": c.APIKey,
		"task":      task,
	}

	jsonData, _ := json.Marshal(payload)
	resp, err := c.Client.Post(CAPSOLVER_API+"/createTask", "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return "", fmt.Errorf("failed to create task: %w", err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	var createResult CreateTaskResponse
	json.Unmarshal(body, &createResult)

	if createResult.ErrorID != 0 {
		return "", fmt.Errorf("API error: %s - %s", createResult.ErrorCode, createResult.ErrorDescription)
	}

	log.Printf("Task created: %s", createResult.TaskID)

	// Poll for result
	for i := 0; i < 120; i++ {
		result, err := c.getTaskResult(createResult.TaskID)
		if err != nil {
			return "", err
		}

		if result.Status == "ready" {
			log.Printf("Turnstile solved successfully!")
			return result.Solution.Token, nil
		}

		if result.Status == "failed" {
			return "", fmt.Errorf("task failed: %s", result.ErrorDescription)
		}

		if i%10 == 0 {
			log.Printf("Waiting for solution... (%ds)", i)
		}
		time.Sleep(1 * time.Second)
	}

	return "", fmt.Errorf("timeout waiting for solution")
}

func (c *CapsolverClient) getTaskResult(taskID string) (*GetTaskResultResponse, error) {
	payload := map[string]string{
		"clientKey": c.APIKey,
		"taskId":    taskID,
	}

	jsonData, _ := json.Marshal(payload)
	resp, err := c.Client.Post(CAPSOLVER_API+"/getTaskResult", "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	var result GetTaskResultResponse
	json.Unmarshal(body, &result)

	return &result, nil
}

// extractTurnstileSiteKey extracts the Turnstile site key from page HTML
func extractTurnstileSiteKey(html string) string {
	// Pattern 1: data-sitekey attribute on cf-turnstile div
	patterns := []string{
		`cf-turnstile[^>]*data-sitekey=['"]([^'"]+)['"]`,
		`data-sitekey=['"]([^'"]+)['"][^>]*class=['"][^'"]*cf-turnstile`,
		`turnstile\.render\s*\([^,]+,\s*\{[^}]*sitekey['":\s]+['"]([^'"]+)['"]`,
		`sitekey['":\s]+['"]([0-9a-zA-Z_-]+)['"]`,
	}

	for _, pattern := range patterns {
		re := regexp.MustCompile(pattern)
		matches := re.FindStringSubmatch(html)
		if len(matches) > 1 {
			return matches[1]
		}
	}

	return ""
}

// injectTurnstileToken injects the solved token into the page
func injectTurnstileToken(page *rod.Page, token string) error {
	js := fmt.Sprintf(`
		(function() {
			// Set the cf-turnstile-response field
			var responseField = document.querySelector('[name="cf-turnstile-response"]');
			if (responseField) {
				responseField.value = '%s';
			}

			// Also try to find by ID
			var byId = document.getElementById('cf-turnstile-response');
			if (byId) {
				byId.value = '%s';
			}

			// Create hidden input if needed
			if (!responseField && !byId) {
				var input = document.createElement('input');
				input.type = 'hidden';
				input.name = 'cf-turnstile-response';
				input.value = '%s';
				var form = document.querySelector('form');
				if (form) {
					form.appendChild(input);
				}
			}

			// Try to trigger callback
			if (window.turnstile && window.turnstileCallback) {
				window.turnstileCallback('%s');
			}

			return true;
		})();
	`, token, token, token, token)

	_, err := page.Eval(js)
	return err
}

func main() {
	// Check API key
	if CAPSOLVER_API_KEY == "" {
		log.Fatal("CAPSOLVER_API_KEY environment variable is required")
	}

	// Target URL - Replace with a site using Cloudflare Turnstile
	targetURL := "https://example.com"

	log.Println("==============================================")
	log.Println("Katana + Capsolver - Turnstile Demo")
	log.Println("==============================================")

	// Initialize Capsolver client
	client := NewCapsolverClient(CAPSOLVER_API_KEY)

	// Check balance
	balance, err := client.GetBalance()
	if err != nil {
		log.Printf("Warning: Could not check balance: %v", err)
	} else {
		log.Printf("Capsolver balance: $%.2f", balance)
	}

	// Launch browser
	log.Println("Launching browser...")
	path, _ := launcher.LookPath()
	u := launcher.New().Bin(path).Headless(true).MustLaunch()
	browser := rod.New().ControlURL(u).MustConnect()
	defer browser.MustClose()

	// Navigate to target
	log.Printf("Navigating to: %s", targetURL)
	page := browser.MustPage(targetURL)
	page.MustWaitLoad()
	time.Sleep(2 * time.Second)

	// Get page HTML
	html := page.MustHTML()

	// Check for Turnstile
	if !strings.Contains(html, "cf-turnstile") && !strings.Contains(html, "turnstile") {
		log.Println("No Turnstile found on page")
		log.Println("Tip: Replace targetURL with a site that uses Cloudflare Turnstile")
		return
	}

	log.Println("Cloudflare Turnstile detected!")

	// Extract site key
	siteKey := extractTurnstileSiteKey(html)
	if siteKey == "" {
		log.Fatal("Could not extract site key")
	}
	log.Printf("Site key: %s", siteKey)

	// Solve Turnstile
	log.Println("Solving Turnstile with Capsolver...")
	token, err := client.SolveTurnstile(targetURL, siteKey)
	if err != nil {
		log.Fatalf("Failed to solve Turnstile: %v", err)
	}

	log.Printf("Token received: %s...", token[:min(50, len(token))])

	// Inject token
	log.Println("Injecting token into page...")
	err = injectTurnstileToken(page, token)
	if err != nil {
		log.Fatalf("Failed to inject token: %v", err)
	}

	log.Println("==============================================")
	log.Println("SUCCESS! Turnstile token injected!")
	log.Println("==============================================")

	// Get page title
	title := page.MustEval(`document.title`).String()
	log.Printf("Page title: %s", title)
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

Turnstile Key Points

  1. Task Type: Use AntiTurnstileTaskProxyLess
  2. Response Field: Turnstile uses cf-turnstile-response instead of g-recaptcha-response
  3. Faster Solving: Turnstile typically solves faster than reCAPTCHA (1-10 seconds)
  4. Token Field: Solution is in solution.token instead of solution.gRecaptchaResponse

Universal CAPTCHA Crawler

Here's a complete, modular crawler that handles all CAPTCHA types automatically:

go Copy
// Universal CAPTCHA Crawler - Complete Example
// Automatically detects and solves reCAPTCHA v2 and Turnstile
// Usage: go run main.go -url "https://example.com"
// Requires: CAPSOLVER_API_KEY environment variable

package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"regexp"
	"strings"
	"time"

	"github.com/go-rod/rod"
	"github.com/go-rod/rod/lib/launcher"
)

// ============================================
// Configuration
// ============================================

var (
	CAPSOLVER_API_KEY = os.Getenv("CAPSOLVER_API_KEY")
	CAPSOLVER_API     = "https://api.capsolver.com"
)

// CaptchaType represents different CAPTCHA types
type CaptchaType string

const (
	RecaptchaV2 CaptchaType = "recaptcha_v2"
	Turnstile   CaptchaType = "turnstile"
	Unknown     CaptchaType = "unknown"
)

// CaptchaInfo contains extracted CAPTCHA parameters
type CaptchaInfo struct {
	Type    CaptchaType
	SiteKey string
}

// ============================================
// API Types
// ============================================

type CreateTaskResponse struct {
	ErrorID          int    `json:"errorId"`
	ErrorCode        string `json:"errorCode"`
	ErrorDescription string `json:"errorDescription"`
	TaskID           string `json:"taskId"`
}

type GetTaskResultResponse struct {
	ErrorID          int    `json:"errorId"`
	ErrorCode        string `json:"errorCode"`
	ErrorDescription string `json:"errorDescription"`
	Status           string `json:"status"`
	Solution         struct {
		GRecaptchaResponse string `json:"gRecaptchaResponse"`
		Token              string `json:"token"`
	} `json:"solution"`
}

type BalanceResponse struct {
	ErrorID int     `json:"errorId"`
	Balance float64 `json:"balance"`
}

// ============================================
// Capsolver Client
// ============================================

type CapsolverClient struct {
	APIKey string
	Client *http.Client
}

func NewCapsolverClient(apiKey string) *CapsolverClient {
	return &CapsolverClient{
		APIKey: apiKey,
		Client: &http.Client{Timeout: 120 * time.Second},
	}
}

func (c *CapsolverClient) GetBalance() (float64, error) {
	payload := map[string]string{"clientKey": c.APIKey}
	jsonData, _ := json.Marshal(payload)

	resp, err := c.Client.Post(CAPSOLVER_API+"/getBalance", "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return 0, err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	var result BalanceResponse
	json.Unmarshal(body, &result)

	return result.Balance, nil
}

func (c *CapsolverClient) Solve(info *CaptchaInfo, websiteURL string) (string, error) {
	switch info.Type {
	case RecaptchaV2:
		return c.solveRecaptchaV2(websiteURL, info.SiteKey)
	case Turnstile:
		return c.solveTurnstile(websiteURL, info.SiteKey)
	default:
		return "", fmt.Errorf("unsupported CAPTCHA type: %s", info.Type)
	}
}

func (c *CapsolverClient) solveRecaptchaV2(websiteURL, siteKey string) (string, error) {
	task := map[string]interface{}{
		"type":       "ReCaptchaV2TaskProxyLess",
		"websiteURL": websiteURL,
		"websiteKey": siteKey,
	}
	return c.solveTask(task, "recaptcha")
}

func (c *CapsolverClient) solveTurnstile(websiteURL, siteKey string) (string, error) {
	task := map[string]interface{}{
		"type":       "AntiTurnstileTaskProxyLess",
		"websiteURL": websiteURL,
		"websiteKey": siteKey,
	}
	return c.solveTask(task, "turnstile")
}

func (c *CapsolverClient) solveTask(task map[string]interface{}, tokenType string) (string, error) {
	// Create task
	payload := map[string]interface{}{
		"clientKey": c.APIKey,
		"task":      task,
	}

	jsonData, _ := json.Marshal(payload)
	resp, err := c.Client.Post(CAPSOLVER_API+"/createTask", "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return "", fmt.Errorf("failed to create task: %w", err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	var createResult CreateTaskResponse
	json.Unmarshal(body, &createResult)

	if createResult.ErrorID != 0 {
		return "", fmt.Errorf("API error: %s - %s", createResult.ErrorCode, createResult.ErrorDescription)
	}

	log.Printf("Task created: %s", createResult.TaskID)

	// Poll for result
	for i := 0; i < 120; i++ {
		getPayload := map[string]string{
			"clientKey": c.APIKey,
			"taskId":    createResult.TaskID,
		}

		jsonData, _ := json.Marshal(getPayload)
		resp, err := c.Client.Post(CAPSOLVER_API+"/getTaskResult", "application/json", bytes.NewBuffer(jsonData))
		if err != nil {
			return "", err
		}

		body, _ := io.ReadAll(resp.Body)
		resp.Body.Close()

		var result GetTaskResultResponse
		json.Unmarshal(body, &result)

		if result.Status == "ready" {
			if tokenType == "turnstile" {
				return result.Solution.Token, nil
			}
			return result.Solution.GRecaptchaResponse, nil
		}

		if result.Status == "failed" {
			return "", fmt.Errorf("task failed: %s", result.ErrorDescription)
		}

		if i%10 == 0 {
			log.Printf("Waiting for solution... (%ds)", i)
		}
		time.Sleep(1 * time.Second)
	}

	return "", fmt.Errorf("timeout waiting for solution")
}

// ============================================
// CAPTCHA Detection
// ============================================

func DetectCaptcha(html string) *CaptchaInfo {
	// Check for reCAPTCHA v2 (checkbox)
	if strings.Contains(html, "g-recaptcha") {
		siteKey := extractDataSiteKey(html, "g-recaptcha")
		if siteKey != "" {
			return &CaptchaInfo{
				Type:    RecaptchaV2,
				SiteKey: siteKey,
			}
		}
	}

	// Check for Cloudflare Turnstile
	if strings.Contains(html, "cf-turnstile") || strings.Contains(html, "challenges.cloudflare.com/turnstile") {
		siteKey := extractDataSiteKey(html, "cf-turnstile")
		if siteKey != "" {
			return &CaptchaInfo{
				Type:    Turnstile,
				SiteKey: siteKey,
			}
		}
	}

	return nil
}

func extractDataSiteKey(html, className string) string {
	pattern := fmt.Sprintf(`class=['"][^'"]*%s[^'"]*['"][^>]*data-sitekey=['"]([^'"]+)['"]`, className)
	re := regexp.MustCompile(pattern)
	matches := re.FindStringSubmatch(html)
	if len(matches) > 1 {
		return matches[1]
	}

	// Alternative pattern
	pattern = fmt.Sprintf(`data-sitekey=['"]([^'"]+)['"][^>]*class=['"][^'"]*%s`, className)
	re = regexp.MustCompile(pattern)
	matches = re.FindStringSubmatch(html)
	if len(matches) > 1 {
		return matches[1]
	}

	// Generic sitekey pattern
	re = regexp.MustCompile(`data-sitekey=['"]([^'"]+)['"]`)
	matches = re.FindStringSubmatch(html)
	if len(matches) > 1 {
		return matches[1]
	}

	return ""
}

// ============================================
// Token Injection
// ============================================

func InjectToken(page *rod.Page, token string, captchaType CaptchaType) error {
	var js string

	switch captchaType {
	case RecaptchaV2:
		js = fmt.Sprintf(`
			(function() {
				var responseField = document.getElementById('g-recaptcha-response');
				if (responseField) {
					responseField.style.display = 'block';
					responseField.value = '%s';
				}

				var textareas = document.querySelectorAll('textarea[name="g-recaptcha-response"]');
				for (var i = 0; i < textareas.length; i++) {
					textareas[i].value = '%s';
				}

				if (typeof ___grecaptcha_cfg !== 'undefined') {
					var clients = ___grecaptcha_cfg.clients;
					for (var key in clients) {
						var client = clients[key];
						if (client) {
							try {
								var callback = client.callback ||
									(client.Q && client.Q.callback) ||
									(client.S && client.S.callback);
								if (typeof callback === 'function') {
									callback('%s');
								}
							} catch(e) {}
						}
					}
				}
				return true;
			})();
		`, token, token, token)

	case Turnstile:
		js = fmt.Sprintf(`
			(function() {
				var responseField = document.querySelector('[name="cf-turnstile-response"]');
				if (responseField) {
					responseField.value = '%s';
				}

				if (!responseField) {
					var input = document.createElement('input');
					input.type = 'hidden';
					input.name = 'cf-turnstile-response';
					input.value = '%s';
					var form = document.querySelector('form');
					if (form) form.appendChild(input);
				}

				if (window.turnstile && window.turnstileCallback) {
					window.turnstileCallback('%s');
				}
				return true;
			})();
		`, token, token, token)

	default:
		return fmt.Errorf("unsupported CAPTCHA type: %s", captchaType)
	}

	_, err := page.Eval(js)
	return err
}

// ============================================
// Crawler
// ============================================

type CrawlResult struct {
	URL           string
	Title         string
	Success       bool
	CaptchaFound  bool
	CaptchaType   CaptchaType
	CaptchaSolved bool
	Error         string
}

func Crawl(browser *rod.Browser, client *CapsolverClient, targetURL string) *CrawlResult {
	result := &CrawlResult{
		URL:     targetURL,
		Success: false,
	}

	// Navigate to target
	log.Printf("Navigating to: %s", targetURL)
	page := browser.MustPage(targetURL)
	defer page.MustClose()

	page.MustWaitLoad()
	time.Sleep(2 * time.Second)

	// Get page HTML
	html := page.MustHTML()

	// Detect CAPTCHA
	captchaInfo := DetectCaptcha(html)

	if captchaInfo != nil && captchaInfo.Type != Unknown {
		result.CaptchaFound = true
		result.CaptchaType = captchaInfo.Type

		log.Printf("CAPTCHA detected: %s (siteKey: %s)", captchaInfo.Type, captchaInfo.SiteKey)

		// Solve CAPTCHA
		log.Println("Solving CAPTCHA with Capsolver...")
		token, err := client.Solve(captchaInfo, targetURL)
		if err != nil {
			result.Error = fmt.Sprintf("failed to solve CAPTCHA: %v", err)
			log.Printf("Error: %s", result.Error)
			return result
		}

		log.Printf("Token received: %s...", token[:min(50, len(token))])

		// Inject token
		log.Println("Injecting token...")
		err = InjectToken(page, token, captchaInfo.Type)
		if err != nil {
			result.Error = fmt.Sprintf("failed to inject token: %v", err)
			log.Printf("Error: %s", result.Error)
			return result
		}

		result.CaptchaSolved = true
		log.Println("Token injected successfully!")

		// Try to submit form
		submitForm(page)
		time.Sleep(3 * time.Second)
	} else {
		log.Println("No CAPTCHA detected on page")
	}

	// Get final page info
	result.Title = page.MustEval(`document.title`).String()
	result.Success = true

	return result
}

func submitForm(page *rod.Page) {
	selectors := []string{
		"button[type='submit']",
		"input[type='submit']",
		"#recaptcha-demo-submit",
		".submit-button",
	}

	for _, selector := range selectors {
		js := fmt.Sprintf(`
			(function() {
				var btn = document.querySelector('%s');
				if (btn && btn.offsetParent !== null) {
					btn.click();
					return true;
				}
				return false;
			})();
		`, selector)

		result := page.MustEval(js)
		if result.Bool() {
			log.Printf("Clicked submit button: %s", selector)
			return
		}
	}
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

// ============================================
// Main
// ============================================

func main() {
	// Parse flags
	targetURL := flag.String("url", "https://www.google.com/recaptcha/api2/demo", "Target URL to crawl")
	headless := flag.Bool("headless", true, "Run browser in headless mode")
	checkBalance := flag.Bool("balance", false, "Only check account balance")
	flag.Parse()

	// Check API key
	if CAPSOLVER_API_KEY == "" {
		log.Fatal("CAPSOLVER_API_KEY environment variable is required")
	}

	log.Println("==============================================")
	log.Println("Katana + Capsolver - Universal CAPTCHA Crawler")
	log.Println("==============================================")

	// Initialize client
	client := NewCapsolverClient(CAPSOLVER_API_KEY)

	// Check balance
	balance, err := client.GetBalance()
	if err != nil {
		log.Printf("Warning: Could not check balance: %v", err)
	} else {
		log.Printf("Capsolver balance: $%.2f", balance)
	}

	if *checkBalance {
		return
	}

	// Launch browser
	log.Println("Launching browser...")
	path, _ := launcher.LookPath()
	u := launcher.New().Bin(path).Headless(*headless).MustLaunch()
	browser := rod.New().ControlURL(u).MustConnect()
	defer browser.MustClose()

	// Crawl
	result := Crawl(browser, client, *targetURL)

	// Output results
	log.Println("==============================================")
	log.Println("CRAWL RESULTS")
	log.Println("==============================================")
	log.Printf("URL: %s", result.URL)
	log.Printf("Title: %s", result.Title)
	log.Printf("Success: %v", result.Success)
	log.Printf("CAPTCHA Found: %v", result.CaptchaFound)
	if result.CaptchaFound {
		log.Printf("CAPTCHA Type: %s", result.CaptchaType)
		log.Printf("CAPTCHA Solved: %v", result.CaptchaSolved)
	}
	if result.Error != "" {
		log.Printf("Error: %s", result.Error)
	}
	log.Println("==============================================")
}

Usage

bash Copy
# Create project
mkdir katana-universal-crawler
cd katana-universal-crawler
go mod init katana-universal-crawler

# Install dependencies
go get github.com/go-rod/rod@latest

# Set API key
export CAPSOLVER_API_KEY="YOUR_API_KEY"

# Run with default (reCAPTCHA v2 demo)
go run main.go

# Run with custom URL
go run main.go -url "https://example.com"

# Check balance only
go run main.go -balance

# Run with visible browser
go run main.go -headless=false

Best Practices

1. Performance Optimization

  • Use ProxyLess Task Types: ReCaptchaV2TaskProxyLess uses Capsolver's internal proxies for faster solving
  • Parallel Processing: Start CAPTCHA solving while other page elements load
  • Token Caching: reCAPTCHA tokens are valid for ~2 minutes; cache when possible

2. Cost Management

  • Detect Before Solving: Only call Capsolver when a CAPTCHA is actually present
  • Validate Site Keys: Ensure extracted keys are valid before API calls
  • Monitor Usage: Track API calls to manage costs effectively

3. Error Handling

go Copy
func SolveWithRetry(client *CapsolverClient, info *CaptchaInfo, url string, maxRetries int) (string, error) {
    var lastErr error

    for i := 0; i < maxRetries; i++ {
        token, err := client.Solve(info, url)
        if err == nil {
            return token, nil
        }

        lastErr = err
        log.Printf("Attempt %d failed: %v", i+1, err)

        // Exponential backoff
        time.Sleep(time.Duration(i+1) * time.Second)
    }

    return "", fmt.Errorf("failed after %d retries: %w", maxRetries, lastErr)
}

4. Rate Limiting

Implement appropriate delays between requests to avoid detection:

go Copy
type RateLimiter struct {
    requests    int
    interval    time.Duration
    lastRequest time.Time
    mu          sync.Mutex
}

func (r *RateLimiter) Wait() {
    r.mu.Lock()
    defer r.mu.Unlock()

    elapsed := time.Since(r.lastRequest)
    if elapsed < r.interval {
        time.Sleep(r.interval - elapsed)
    }
    r.lastRequest = time.Now()
}

Troubleshooting

Common Errors

Error Cause Solution
ERROR_ZERO_BALANCE Insufficient credits Top up Capsolver account
ERROR_CAPTCHA_UNSOLVABLE Invalid site key Verify extraction logic
ERROR_INVALID_TASK_DATA Missing parameters Check task struct
context deadline exceeded Timeout Increase timeout or check network

Debugging Tips

  1. Enable Visible Browser: Set Headless(false) to see what's happening
  2. Log Network Traffic: Monitor requests to identify issues
  3. Save Screenshots: Capture page state for debugging
  4. Validate Tokens: Log token format before injection

FAQ

Q: Can I use Katana without headless mode for CAPTCHA pages?
A: No, CAPTCHA pages require JavaScript rendering which only works in headless mode.

Q: How long are CAPTCHA tokens valid?
A: reCAPTCHA tokens: ~2 minutes. Turnstile: varies by configuration.

Q: What's the average solve time?
A: reCAPTCHA v2: 5-15s, Turnstile: 1-10s.

Q: Can I use my own proxy?
A: Yes, use task types without "ProxyLess" suffix and provide proxy config.


Conclusion

Integrating Capsolver with Katana enables robust CAPTCHA handling for your web crawling needs. The complete scripts above can be copied directly and used with your Go projects.

Ready to start? Sign up for Capsolver and supercharge your crawlers!

💡 Exclusive Bonus for Katana Integration Users:
To celebrate this integration, we're offering an exclusive 6% bonus code — Katana for all CapSolver users who register through this tutorial.
Simply enter the code during recharge in Dashboard to receive an extra 6% credit instantly.


12. Documentations


Compliance Disclaimer: The information provided on this blog is for informational purposes only. CapSolver is committed to compliance with all applicable laws and regulations. The use of the CapSolver network for illegal, fraudulent, or abusive activities is strictly prohibited and will be investigated. Our captcha-solving solutions enhance user experience while ensuring 100% compliance in helping solve captcha difficulties during public data crawling. We encourage responsible use of our services. For more information, please visit our Terms of Service and Privacy Policy.

More

How to Solve Captcha with Katana Using CapSolver
Integrating Katana with CapSolver: Automated CAPTCHA Solving for Web Crawling

Learn how to integrate Katana with Capsolver to automatically solve reCAPTCHA v2 and Cloudflare Turnstile in headless crawling.

web scraping
Logo of CapSolver

Lucas Mitchell

09-Jan-2026

How to Solve Captcha with Crawlab Using CapSolver
Integrating Crawlab with CapSolver: Automated CAPTCHA Solving for Distributed Crawling

Learn how to integrate CapSolver with Crawlab to solve reCAPTCHA and Cloudflare Turnstile at scale.

web scraping
Logo of CapSolver

Ethan Collins

09-Jan-2026

Top Python Web Scraping Libraries 2026
Top Python Web Scraping Libraries 2026

Explore the best Python web scraping libraries for 2026. Compare features, ease of use, and performance for your data extraction needs. Includes expert insights and FAQs.

web scraping
Logo of CapSolver

Emma Foster

09-Jan-2026

6 Best Web Unblockers Compared
6 Best Web Unblockers Compared: Best Options in 2026

Compare the 6 best web unblockers in 2026. Discover top-rated web unblocker APIs like Decodo, Oxylabs, and Bright Data for bypassing anti-bot systems, residential proxies, and automated scraping tools.

web scraping
Logo of CapSolver

Ethan Collins

07-Jan-2026

The Best AI Scraping Tools You Must Know in 2026
The Best AI Scraping Tools You Must Know in 2026

Discover the Best AI Scraping tool options for 2026. We compare top AI web scraping tools, including Bright Data, Crawl4AI, and Browse AI, with specific pricing to help you master automated data extraction and security challenge resolution.

web scraping
Logo of CapSolver

Emma Foster

07-Jan-2026

Best Alternative Data Providers
Best Alternative Data Providers in 2026 (Top Platforms Compared)

Discover the best Alternative Data Providers in 2026. Our guide compares top platforms (YipitData, FactSet, Preqin) with pros, cons, and pricing insights for compliance and alpha generation.

web scraping
Logo of CapSolver

Lucas Mitchell

05-Jan-2026