Integrating Katana with CapSolver: Automated CAPTCHA Solving for Web Crawling

Lucas Mitchell
Automation Engineer
09-Jan-2026

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
# Requires Go 1.24+
CGO_ENABLED=1 go install github.com/projectdiscovery/katana/cmd/katana@latest
Basic Usage
bash
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:
- Create Task: Submit CAPTCHA parameters (type, siteKey, URL)
- Get Task ID: Receive a unique task identifier
- Poll for Result: Check task status until solution is ready
- Receive Token: Get the solved CAPTCHA token
Prerequisites
Before starting, ensure you have:
- Go 1.24+ installed
- Capsolver API Key - Sign up here
- Chrome Browser (for headless mode)
Set your API key as an environment variable:
bash
export CAPSOLVER_API_KEY="YOUR_API_KEY"
Integration Architecture
┌─────────────────────────┐
│ 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
// 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
# 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
// 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
- Task Type: Use
AntiTurnstileTaskProxyLess - Response Field: Turnstile uses
cf-turnstile-responseinstead ofg-recaptcha-response - Faster Solving: Turnstile typically solves faster than reCAPTCHA (1-10 seconds)
- Token Field: Solution is in
solution.tokeninstead ofsolution.gRecaptchaResponse
Universal CAPTCHA Crawler
Here's a complete, modular crawler that handles all CAPTCHA types automatically:
go
// 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
# 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:
ReCaptchaV2TaskProxyLessuses 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
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
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
- Enable Visible Browser: Set
Headless(false)to see what's happening - Log Network Traffic: Monitor requests to identify issues
- Save Screenshots: Capture page state for debugging
- 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
- 12.1. Katana GitHub Repository
- 12.2. Katana Documentation
- 12.3. Capsolver Documentation
- 12.4. Go Rod Browser Automation
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

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.

Lucas Mitchell
09-Jan-2026

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.

Ethan Collins
09-Jan-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.

Emma Foster
09-Jan-2026

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.

Ethan Collins
07-Jan-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.

Emma Foster
07-Jan-2026

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.

Lucas Mitchell
05-Jan-2026


