API-Tutorials

Bash-Skript + cURL + CaptchaAI: Shell-CAPTCHA-Automatisierung

Muss auf dem Server wirklich ein Python-Interpreter installiert sein, nur damit ein nächtlicher Cron-Job an einer CAPTCHA-Abfrage nicht hängen bleibt?

Nein – Bash, curl und jq genügen: Aufgabe an in.php senden, Token von res.php abholen, Token in das Formularfeld eintragen.

Dieser Leitfaden zeigt den Ablauf in reinem Bash: reCAPTCHA v2 und v3, Cloudflare Turnstile, Bild-CAPTCHAs, dazu eine wiederverwendbare Bibliothek, paralleles Lösen und den Betrieb im Container.


Zwei Endpunkte, mehr passiert nicht

Die HTTP-API von CaptchaAI kommt mit zwei Adressen aus:

  1. Aufgabe per POST an https://ocr.captchaai.com/in.php schicken.
  2. Task-ID aus der Antwort lesen.
  3. res.php mit dieser ID abfragen, bis statt CAPCHA_NOT_READY das Ergebnis kommt – bei Bild-CAPTCHAs der erkannte Text, bei reCAPTCHA das Token für g-recaptcha-response.

Diesen Weg nehmen alle unterstützten Typen: reCAPTCHA v2 samt Invisible-, Callback- und Enterprise-Variante, reCAPTCHA v3, Cloudflare Turnstile und Challenge, GeeTest v3, Bild-, Rasterbild- und BLS-CAPTCHAs, dazu CaptchaFox (Beta), Friendly Captcha (Beta) und Lemin (Beta).

hCaptcha und FunCaptcha (Arkose Labs) werden nicht unterstützt, GeeTest v4 ist als bald verfügbar angekündigt.

Das Polling-Intervall richtet sich nach dem Typ: Bild-CAPTCHAs liegen bei unter 0,5 s, reCAPTCHA v3 bei unter 4 s, Turnstile bei unter 10 s, reCAPTCHA v2 bei unter 60 s.

Fünf Sekunden Abstand passen fast immer.


Wann Bash reicht – und wann nicht

  • Keine Abhängigkeiten – Bash und curl liegen auf jedem Linux- und macOS-System vor, jq ist der einzige Zusatz.
  • Kein Deployment-Aufwand – kein Interpreter, kein virtuelles Environment, kein node_modules auf dem Server.
  • Cron-tauglich – ein Skript, ein crontab-Eintrag, ein Logfile.
  • CI/CD-tauglich – läuft unverändert in GitLab CI, GitHub Actions, Jenkins oder einem Alpine-Container.
  • Kombinierbar – Ergebnisse gehen direkt weiter an jq, grep oder awk.

Die Grenze ist schnell erreicht: Sobald eine Browser-Sitzung, JavaScript oder ein mehrstufiger Formularzustand im Spiel ist, führt der Weg über Selenium, Playwright oder Puppeteer.


Was Sie brauchen

  • Bash 4.0 oder neuer
  • curl (unter Linux und macOS vorinstalliert)
  • jq für das JSON-Parsing: apt install jq bzw. brew install jq
  • einen CaptchaAI-API-Schlüssel – Konto hier anlegen

Hinterlegen Sie den Schlüssel als Umgebungsvariable (export CAPTCHAAI_KEY=...), nicht als Literal im Skript.


Die beiden Kernfunktionen

Aufgabe übermitteln

#!/bin/bash

CAPTCHAAI_URL="https://ocr.captchaai.com"

submit_task() {
    local api_key="$1"
    shift
    local params=("$@")

    local response
    response=$(curl -s -X POST "${CAPTCHAAI_URL}/in.php" \
        -d "key=${api_key}" \
        -d "json=1" \
        "${params[@]}")

    local status
    status=$(echo "$response" | jq -r '.status')
    local request
    request=$(echo "$response" | jq -r '.request')

    if [ "$status" != "1" ]; then
        echo "ERROR: Submit failed: $request" >&2
        return 1
    fi

    echo "$request"
}

submit_task reicht alle weiteren -d-Parameter durch und gibt die Task-ID zurück.

json=1 sorgt für eine JSON-Antwort; ohne das Flag kommt das ältere Textformat.

Ergebnis abfragen

poll_result() {
    local api_key="$1"
    local task_id="$2"
    local max_wait="${3:-300}"
    local interval="${4:-5}"

    local elapsed=0

    while [ "$elapsed" -lt "$max_wait" ]; do
        sleep "$interval"
        elapsed=$((elapsed + interval))

        local response
        response=$(curl -s "${CAPTCHAAI_URL}/res.php?key=${api_key}&action=get&id=${task_id}&json=1")

        local status
        status=$(echo "$response" | jq -r '.status')
        local request
        request=$(echo "$response" | jq -r '.request')

        if [ "$request" = "CAPCHA_NOT_READY" ]; then
            echo "Waiting... (${elapsed}s/${max_wait}s)" >&2
            continue
        fi

        if [ "$status" != "1" ]; then
            echo "ERROR: Solve failed: $request" >&2
            return 1
        fi

        echo "$request"
        return 0
    done

    echo "ERROR: Timeout after ${max_wait}s" >&2
    return 1
}

Die Schleife wartet zuerst und fragt dann ab – direkt nach dem Absenden kommt ohnehin CAPCHA_NOT_READY.

Nach 300 s bricht sie ab, damit kein hängender Job den nächsten Cron-Slot blockiert.


reCAPTCHA v2 lösen

Sie brauchen zwei Werte von der Zielseite: den Sitekey (data-sitekey) und die vollständige Page-URL.

Beide gehen als googlekey und pageurl an in.php.

solve_recaptcha_v2() {
    local api_key="$1"
    local site_url="$2"
    local sitekey="$3"

    echo "Submitting reCAPTCHA v2..." >&2
    local task_id
    task_id=$(submit_task "$api_key" \
        -d "method=userrecaptcha" \
        -d "googlekey=${sitekey}" \
        -d "pageurl=${site_url}")

    if [ $? -ne 0 ]; then return 1; fi
    echo "Task ID: $task_id" >&2

    echo "Polling for solution..." >&2
    local token
    token=$(poll_result "$api_key" "$task_id")

    if [ $? -ne 0 ]; then return 1; fi
    echo "$token"
}

# Usage
API_KEY="YOUR_API_KEY"
TOKEN=$(solve_recaptcha_v2 "$API_KEY" \
    "https://example.com/login" \
    "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-")

echo "Token: ${TOKEN:0:50}..."

Cloudflare Turnstile lösen

Turnstile folgt demselben Muster, nur mit method=turnstile.

Der Sitekey beginnt hier üblicherweise mit 0x.

solve_turnstile() {
    local api_key="$1"
    local site_url="$2"
    local sitekey="$3"

    local task_id
    task_id=$(submit_task "$api_key" \
        -d "method=turnstile" \
        -d "key=${sitekey}" \
        -d "pageurl=${site_url}")

    if [ $? -ne 0 ]; then return 1; fi

    poll_result "$api_key" "$task_id"
}

# Usage
TOKEN=$(solve_turnstile "$API_KEY" \
    "https://example.com/form" \
    "0x4AAAAAAAB5...")

reCAPTCHA v3 mit passender Action

reCAPTCHA v3 zeigt keine sichtbare Abfrage, sondern vergibt einen Score.

Übergeben Sie deshalb version=v3 und genau die Action der Seite – ein abweichender Wert drückt den Score.

solve_recaptcha_v3() {
    local api_key="$1"
    local site_url="$2"
    local sitekey="$3"
    local action="${4:-verify}"

    local task_id
    task_id=$(submit_task "$api_key" \
        -d "method=userrecaptcha" \
        -d "googlekey=${sitekey}" \
        -d "pageurl=${site_url}" \
        -d "version=v3" \
        -d "action=${action}" \

    if [ $? -ne 0 ]; then return 1; fi

    poll_result "$api_key" "$task_id"
}

Bild-CAPTCHAs per OCR lösen

Bild-CAPTCHAs gehen Base64-kodiert an method=base64.

Achten Sie auf den Plattformunterschied: Linux braucht base64 -w 0, das BSD-Pendant unter macOS nicht – die Funktion fängt beides ab.

solve_image_captcha() {
    local api_key="$1"
    local image_path="$2"

    if [ ! -f "$image_path" ]; then
        echo "ERROR: File not found: $image_path" >&2
        return 1
    fi

    local base64_data
    base64_data=$(base64 -w 0 "$image_path" 2>/dev/null || base64 "$image_path")

    local task_id
    task_id=$(submit_task "$api_key" \
        -d "method=base64" \
        --data-urlencode "body=${base64_data}")

    if [ $? -ne 0 ]; then return 1; fi

    poll_result "$api_key" "$task_id"
}

# From URL
solve_image_from_url() {
    local api_key="$1"
    local image_url="$2"
    local tmp_file
    tmp_file=$(mktemp /tmp/captcha_XXXXXX.png)

    curl -s -o "$tmp_file" "$image_url"
    local result
    result=$(solve_image_captcha "$api_key" "$tmp_file")
    rm -f "$tmp_file"

    echo "$result"
}

# Usage
TEXT=$(solve_image_captcha "$API_KEY" "captcha.png")
echo "CAPTCHA text: $TEXT"

Die Bibliothek captchaai.sh

Statt die Funktionen in jedes Skript zu kopieren, legen Sie sie einmal ab und binden sie per source ein.

Dazu kommt captchaai_balance für die Guthabenabfrage.

#!/bin/bash
# CaptchaAI Solver Library
# Source this file: source ./captchaai.sh

CAPTCHAAI_URL="https://ocr.captchaai.com"
CAPTCHAAI_POLL_INTERVAL=5
CAPTCHAAI_MAX_WAIT=300

captchaai_submit() {
    local api_key="$1"; shift
    local response
    response=$(curl -s -X POST "${CAPTCHAAI_URL}/in.php" \
        -d "key=${api_key}" -d "json=1" "$@")
    local status=$(echo "$response" | jq -r '.status')
    local request=$(echo "$response" | jq -r '.request')
    [ "$status" = "1" ] && echo "$request" || { echo "Submit: $request" >&2; return 1; }
}

captchaai_poll() {
    local api_key="$1" task_id="$2" elapsed=0
    while [ "$elapsed" -lt "$CAPTCHAAI_MAX_WAIT" ]; do
        sleep "$CAPTCHAAI_POLL_INTERVAL"
        elapsed=$((elapsed + CAPTCHAAI_POLL_INTERVAL))
        local resp=$(curl -s "${CAPTCHAAI_URL}/res.php?key=${api_key}&action=get&id=${task_id}&json=1")
        local req=$(echo "$resp" | jq -r '.request')
        local st=$(echo "$resp" | jq -r '.status')
        [ "$req" = "CAPCHA_NOT_READY" ] && continue
        [ "$st" = "1" ] && { echo "$req"; return 0; }
        echo "Solve: $req" >&2; return 1
    done
    echo "Timeout" >&2; return 1
}

captchaai_balance() {
    local api_key="$1"
    curl -s "${CAPTCHAAI_URL}/res.php?key=${api_key}&action=getbalance&json=1" | jq -r '.request'
}

captchaai_recaptcha_v2() {
    local key="$1" url="$2" sk="$3"
    local tid=$(captchaai_submit "$key" -d "method=userrecaptcha" -d "googlekey=$sk" -d "pageurl=$url") || return 1
    captchaai_poll "$key" "$tid"
}

captchaai_turnstile() {
    local key="$1" url="$2" sk="$3"
    local tid=$(captchaai_submit "$key" -d "method=turnstile" -d "sitekey=$sk" -d "pageurl=$url") || return 1
    captchaai_poll "$key" "$tid"
}

captchaai_image() {
    local key="$1" path="$2"
    local b64=$(base64 -w 0 "$path" 2>/dev/null || base64 "$path")
    local tid=$(captchaai_submit "$key" -d "method=base64" --data-urlencode "body=$b64") || return 1
    captchaai_poll "$key" "$tid"
}

Bibliothek einbinden

#!/bin/bash
source ./captchaai.sh

API_KEY="YOUR_API_KEY"

# Check balance
echo "Balance: $(captchaai_balance "$API_KEY")"

# Solve reCAPTCHA v2
TOKEN=$(captchaai_recaptcha_v2 "$API_KEY" \
    "https://example.com/login" \
    "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-")

echo "Token: ${TOKEN:0:50}..."

Token eintragen und Formular absenden

Ein Token nützt nur, solange es gültig ist: Bei reCAPTCHA verfällt es nach rund 120 Sekunden.

Lösen Sie deshalb erst, wenn alle übrigen Felder feststehen, und senden Sie direkt danach ab.

submit_form_with_token() {
    local url="$1"
    local token="$2"
    shift 2

    curl -s -X POST "$url" \
        -d "g-recaptcha-response=${token}" \
        "$@"
}

# Usage: solve then submit
TOKEN=$(captchaai_recaptcha_v2 "$API_KEY" \
    "https://example.com/login" "SITEKEY")

RESPONSE=$(submit_form_with_token "https://example.com/login" \
    "$TOKEN" \
    -d "[email protected]" \
    -d "password=password")

echo "Response: $RESPONSE"

Mehrere CAPTCHAs parallel lösen

Jeder Hintergrundjob belegt einen Thread Ihres Plans.

Genau darauf zielt die Abrechnung von CaptchaAI: bezahlt wird pro gleichzeitigem Thread, nicht pro Lösung – bei unbegrenzten Lösungen pro Thread im Abrechnungsmonat.

BASIC (15 $/Monat, 5 Threads) trägt ein kleines Cron-Setup, ADVANCE (90 $/Monat, 50 Threads) parallele Exportläufe, VIP-1 (1.500 $/Monat, 1.000 Threads) Dauerlast. Preise in US-Dollar.

#!/bin/bash
source ./captchaai.sh

API_KEY="YOUR_API_KEY"
RESULTS_DIR=$(mktemp -d)

# Define tasks
declare -A TASKS
TASKS["site-a"]="https://site-a.com|SITEKEY_A"
TASKS["site-b"]="https://site-b.com|SITEKEY_B"
TASKS["site-c"]="https://site-c.com|SITEKEY_C"

# Launch parallel solves
pids=()
for name in "${!TASKS[@]}"; do
    IFS='|' read -r url sitekey <<< "${TASKS[$name]}"
    (
        token=$(captchaai_recaptcha_v2 "$API_KEY" "$url" "$sitekey" 2>/dev/null)
        if [ $? -eq 0 ]; then
            echo "$token" > "${RESULTS_DIR}/${name}.token"
        else
            echo "FAILED" > "${RESULTS_DIR}/${name}.token"
        fi
    ) &
    pids+=($!)
done

# Wait for all
for pid in "${pids[@]}"; do
    wait "$pid"
done

# Collect results
echo "=== Results ==="
for name in "${!TASKS[@]}"; do
    token=$(cat "${RESULTS_DIR}/${name}.token")
    if [ "$token" = "FAILED" ]; then
        echo "$name: FAILED"
    else
        echo "$name: ${token:0:50}..."
    fi
done

rm -rf "$RESULTS_DIR"

Mehr gleichzeitige Jobs als gebuchte Threads bringen nichts – die überzähligen Aufgaben laufen in ERROR_NO_SLOT_AVAILABLE.


Wiederholungslogik mit exponentiellem Backoff

Nicht jeder Fehler rechtfertigt einen Abbruch.

ERROR_NO_SLOT_AVAILABLE und ERROR_CAPTCHA_UNSOLVABLE sind vorübergehend, ein falscher Schlüssel oder leeres Guthaben nicht.

solve_with_retry() {
    local api_key="$1"
    local solve_cmd="$2"
    shift 2
    local max_retries="${1:-3}"

    local retryable_errors=("ERROR_NO_SLOT_AVAILABLE" "ERROR_CAPTCHA_UNSOLVABLE")
    local attempt=0

    while [ "$attempt" -le "$max_retries" ]; do
        if [ "$attempt" -gt 0 ]; then
            local delay=$((2 ** attempt + RANDOM % 3))
            echo "Retry $attempt/$max_retries after ${delay}s..." >&2
            sleep "$delay"
        fi

        local result
        result=$($solve_cmd "$api_key" "${@:2}")

        if [ $? -eq 0 ]; then
            echo "$result"
            return 0
        fi

        # Check if error is retryable
        local is_retryable=0
        for err in "${retryable_errors[@]}"; do
            if echo "$result" | grep -q "$err"; then
                is_retryable=1
                break
            fi
        done

        if [ "$is_retryable" -eq 0 ]; then
            echo "$result"
            return 1
        fi

        attempt=$((attempt + 1))
    done

    echo "Max retries exceeded" >&2
    return 1
}

Betrieb als Cron-Job

# Edit crontab: crontab -e
# Run daily at 8 AM
0 8 * * * /path/to/captcha-automation.sh >> /var/log/captcha.log 2>&1

Beispielskript für den nächtlichen Lauf

#!/bin/bash
source /path/to/captchaai.sh

API_KEY="YOUR_API_KEY"
LOG_FILE="/var/log/captcha-$(date +%Y%m%d).log"

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"; }

# Check balance first
BALANCE=$(captchaai_balance "$API_KEY")
log "Balance: $BALANCE"

if (( $(echo "$BALANCE < 1.0" | bc -l) )); then
    log "WARNING: Low balance!"
    exit 1
fi

# Solve and process
TOKEN=$(captchaai_recaptcha_v2 "$API_KEY" \
    "https://portal.example.com" "SITEKEY")

if [ $? -eq 0 ]; then
    log "Solved successfully"
    # Submit form, download data, etc.
    curl -s "https://portal.example.com/data" \
        -d "g-recaptcha-response=$TOKEN" \
        -o "/data/export-$(date +%Y%m%d).csv"
    log "Data exported"
else
    log "ERROR: Failed to solve CAPTCHA"
    exit 1
fi

Zwei Details entscheiden über die Betriebsruhe: die Guthabenprüfung vor dem ersten Versuch und ein Logfile mit Zeitstempel – ohne Token und API-Schlüssel.


Im Container ausführen

Alpine mit bash, curl und jq ergibt ein Image von rund 10 MB.

FROM alpine:3.19

RUN apk add --no-cache bash curl jq

COPY captchaai.sh /usr/local/lib/captchaai.sh
COPY automation.sh /app/automation.sh

RUN chmod +x /app/automation.sh

CMD ["/app/automation.sh"]

In GitLab CI – im deutschsprachigen Raum neben GitHub Actions der Standard – hinterlegen Sie den Schlüssel als maskierte CI/CD-Variable und rufen das Skript im script:-Block auf.


Praxisbeispiel: nächtlicher Datenexport auf einem VPS

Ein typisches Szenario aus dem DACH-Raum: Eine Agentur zieht jede Nacht um 03:00 Uhr Bestandsdaten aus einem Lieferantenportal, dessen Login mit reCAPTCHA v2 abgesichert ist.

Der Stack: ein kleiner VPS bei Hetzner oder netcup, ein crontab-Eintrag, captchaai.sh und rund 60 Zeilen Export-Skript – ein Lauf pro Nacht belegt genau einen Thread.

Zwei Punkte klären Sie vorab selbst: die AGB und Nutzungsbedingungen des Portals sowie den Umgang mit IP-Adressen, die nach DSGVO personenbezogene Daten sind.

Enthält Ihr Export solche Felder, brauchen Sie Rechtsgrundlage und Löschkonzept.


Fehlerbehebung

Meldung Ursache Behebung
ERROR_WRONG_USER_KEY Schlüssel falsch oder nicht gesetzt $CAPTCHAAI_KEY prüfen – im Cron-Kontext fehlt oft der Export
ERROR_ZERO_BALANCE Guthaben aufgebraucht Konto aufladen, captchaai_balance vorab abfragen
ERROR_NO_SLOT_AVAILABLE alle Threads belegt Parallelität senken oder Plan mit mehr Threads wählen
dauerhaft CAPCHA_NOT_READY Parameter unvollständig Sitekey und Page-URL prüfen
jq: command not found jq fehlt im Image apt install jq bzw. brew install jq
base64: invalid option -- 'w' BSD-Variante unter macOS base64 datei statt base64 -w 0 datei
leere Antwort Netzwerkproblem curl mit -v aufrufen

Fazit: Für HTTP-basierte Automatisierung ist die Shell kein Kompromiss, sondern der kürzeste Weg.


Häufige Fragen

Wie viele CAPTCHAs kann ich parallel aus einem Skript lösen?

So viele, wie Ihr Plan Threads umfasst: mit BASIC (15 $/Monat) 5 gleichzeitige Aufgaben, mit ADVANCE (90 $/Monat) 50. Die Zahl der Lösungen pro Thread ist nicht begrenzt.

Warum liefert res.php immer wieder CAPCHA_NOT_READY?

Weil die Aufgabe noch bearbeitet wird – in den ersten Sekunden ist das normal. Bleibt die Meldung bis zum Timeout bestehen, stimmt meist der Sitekey oder die pageurl nicht.

Wie lange ist ein gelöstes Token gültig?

Bei reCAPTCHA rund 120 Sekunden. Zwischen Abholen und Absenden sollte deshalb möglichst wenig passieren; Token auf Vorrat zu halten führt nur zu abgelehnten Formularen.

Lässt sich hCaptcha auf diesem Weg lösen?

Nein. hCaptcha und FunCaptcha (Arkose Labs) unterstützt CaptchaAI nicht, GeeTest v4 ist als bald verfügbar angekündigt. Verfügbar sind reCAPTCHA v2 und v3, Turnstile, Cloudflare Challenge, GeeTest v3 sowie Bild-, Rasterbild- und BLS-CAPTCHAs.

Funktioniert das Skript auch unter Windows?

Ja, über WSL oder Git Bash. Für PowerShell nativ siehe den verlinkten Windows-Leitfaden.


Verwandte Leitfäden


CAPTCHAs aus der Kommandozeile lösen – Schlüssel holen und den ersten Cron-Job scharf schalten.

Kommentare sind für diesen Artikel deaktiviert.